Tiny'Wo | 小窝

网络中的一小块自留地

面试官:说说如何�?React 项目中应�?TypeScript�?

一、前言

单独的使�?TypeScript 并不会导致学习成本很高,但是绝大部分前端开发者的项目都是依赖于框架的

例如�?VueReact 这些框架结合使用的时候,会有一定的门槛

使用 TypeScript 编写 React 代码,除了需�?TypeScript 这个库之外,还需要安�?@types/react@types/react-dom

1
2
3
npm i @types/react -s

npm i @types/react-dom -s

至于上述使用 @types 的库的原因在于,目前非常多的 JavaScript 库并没有提供自己关于 TypeScript 的声明文�?
所以,ts 并不知道这些库的类型以及对应导出的内容,这里 @types 实际就是社区中的 DefinitelyTyped 库,定义了目前市面上绝大多数�?JavaScript 库的声明

所以下载相关的 JavaScript 对应�?@types 声明时,就能够使用使用该库对应的类型定义

二、使用方�?

在编�?React 项目的时候,最常见的使用的组件就是�?

  • 无状态组�?- 有状态组�?- 受控组件

无状态组�?

主要作用是用于展�?UI,如果使�?js 声明,则如下所示:

1
2
3
4
5
6
7
import * as React from "React";

export const Logo = (props) => {
const { logo, className, alt } = props;

return <img src={logo} className={className} alt={alt} />;
};

但这时�?ts 会出现报错提示,原因在于没有定义 porps 类型,这时候就可以使用 interface 接口去定�?porps 即可,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import * as React from "React";

interface IProps {
logo?: string;
className?: string;
alt?: string;
}

export const Logo = (props: IProps) => {
const { logo, className, alt } = props;

return <img src={logo} className={className} alt={alt} />;
};

但是我们都知�?props 里面存在 children 属性,我们不可能每�?porps 接口里面定义多一�?children,如下:

1
2
3
4
5
6
interface IProps {
logo?: string;
className?: string;
alt?: string;
children?: ReactNode;
}

更加规范的写法是使用 React 里面定义好的 FC 属性,里面已经定义�?children 类型,如下:

1
2
3
4
5
export const Logo: React.FC<IProps> = (props) => {
const { logo, className, alt } = props;

return <img src={logo} className={className} alt={alt} />;
};
  • React.FC 显式地定义了返回类型,其他方式是隐式推导�?
  • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全
  • React.FC �?children 提供了隐式的类型(ReactElement | null�?

有状态组�?

可以是一个类组件且存�?props �?state 属�?
如果使用 TypeScript 声明则如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as React from "React";

interface IProps {
color: string;
size?: string;
}
interface IState {
count: number;
}
class App extends React.Component<IProps, IState> {
public state = {
count: 1,
};
public render() {
return <div>Hello world</div>;
}
}

上述通过泛型�?propsstate 进行类型定义,然后在使用的时候就可以在编译器中获取更好的智能提示

关于 Component 泛型类的定义,可以参考下 React 的类型定义文�?node_modules/@types/React/index.d.ts,如下所示:

1
2
3
4
5
class Component<P, S> {
readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>;

state: Readonly<S>;
}

从上述可以看到,state 属性也定义了可读类型,目的是为了防止直接调�?this.state 更新状�?

受控组件

受控组件的特性在于元素的内容通过组件的状�?state 进行控制

由于组件内部的事件是合成事件,不等同于原生事件,

例如一�?input 组件修改内部的状态,常见的定义的时候如下所示:

1
2
3
private updateValue(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({ itemText: e.target.value })
}

常用 Event 事件对象类型�?

  • ClipboardEvent<T = Element> 剪贴板事件对�?- DragEvent<T = Element> 拖拽事件对象
  • ChangeEvent<T = Element> Change 事件对象
  • KeyboardEvent<T = Element> 键盘事件对象
  • MouseEvent<T = Element> 鼠标事件对象
  • TouchEvent<T = Element> 触摸事件对象
  • WheelEvent<T = Element> 滚轮事件对象
  • AnimationEvent<T = Element> 动画事件对象
  • TransitionEvent<T = Element> 过渡事件对象

T 接收一�?DOM 元素类型

三、总结

上述只是简单的�?React 项目使用 TypeScript,但在编�?React 项目的时候,还存�?hooks、默认参数、以�?store 等等……

TypeScript 在框架中使用的学习成本相对会更高,需要不断编写才能熟�?

参考文�?

面试官:说说你对 TypeScript 的理解?�?JavaScript 的区别?

一、是什�?

TypeScript �?JavaScript 的类型的超集,支持ES6语法,支持面向对象编程的概念,如类、接口、继承、泛型等

超集,不得不说另外一个概念,子集,怎么理解这两个呢,举个例子,如果一个集�?A 里面的的所有元素集�?B 里面都存在,那么我们可以理解集合 B 是集�?A 的超集,集合 A 为集�?B 的子�?

其是一种静态类型检查的语言,提供了类型注解,在代码编译阶段就可以检查出数据类型的错�?
同时扩展了 JavaScript 的语法,所以任何现有的 JavaScript 程序可以不加改变的在 TypeScript 下工�?
为了保证兼容性,TypeScript 在编译阶段需要编译器编译成纯 JavaScript 来运行,是为大型应用之开发而设计的语言,如下:

ts 文件如下�?

1
2
const hello: string = "Hello World!";
console.log(hello);

编译文件后:

1
2
const hello = "Hello World!";
console.log(hello);

二、特�?

TypeScript 的特性主要有如下�?

  • *类型批注和编译时类型检�? :在编译时批注变量类�?- 类型推断:ts 中没有批注变量类型会自动推断变量的类�?- 类型擦除:在编译过程中批注的内容和接口会在运行时利用工具擦除
  • 接口:ts 中用接口来定义对象类�?- 枚举:用于取值被限定在一定范围内的场�?- Mixin:可以接受任意类型的�?- 泛型编程:写代码时使用一些以后才指定的类�?- 名字空间:名字只在该区域内有效,其他区域可重复使用该名字而不冲突
  • 元组:元组合并了不同类型的对象,相当于一个可以装不同类型数据的数�?- …

类型批注

通过类型批注提供在编译时启动类型检查的静态类型,这是可选的,而且可以忽略而使�?JavaScript 常规的动态类�?

1
2
3
function Add(left: number, right: number): number {
return left + right;
}

对于基本类型的批注是 numberbool �?string,而弱或动态类型的结构则是 any 类型

类型推断

当类型没有给出时,TypeScript 编译器利用类型推断来推断类型,如下:

1
let str = "string";

变量 str 被推断为字符串类型,这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时

如果缺乏声明而不能推断出类型,那么它的类型被视作默认的动�?any 类型

接口

接口简单来说就是用来描述对象的类型 数据的类型有 numbernull string 等数据格式,对象的类型就是用接口来描述的

1
2
3
4
5
6
7
8
9
interface Person {
name: string;
age: number;
}

let tom: Person = {
name: "Tom",
age: 25,
};

三、区�?

  • TypeScript �?JavaScript 的超集,扩展�?JavaScript 的语�?- TypeScript 可处理已有的 JavaScript 代码,并只对其中�?TypeScript 代码进行编译
  • TypeScript 文件的后缀�?.ts �?ts�?tsx�?dts),JavaScript 文件�?.js
  • 在编�?TypeScript 的文件的时候就会自动编译成 js 文件

更多的区别如下图所示:

参考文�?

面试官:说说如何在Vue项目中应用TypeScript�?

一、前言

与link类似

VUE项目中应用typescript,我们需要引入一个库vue-property-decorator�?
其是基于vue-class-component库而来,这个库vue官方推出的一个支持使用class方式来开发vue单文件组件的�?
主要的功能如下:

  • methods 可以直接声明为类的成员方�?- 计算属性可以被声明为类的属性访问器
  • 初始化的 data 可以被声明为类属�?- data、render 以及所有的 Vue 生命周期钩子可以直接作为类的成员方法
  • 所有其他属性,需要放在装饰器�?

二、使�?

vue-property-decorator 主要提供了多个装饰器和一个函�?

  • @Prop
  • @PropSync
  • @Model
  • @Watch
  • @Provide
  • @Inject
  • @ProvideReactive
  • @InjectReactive
  • @Emit
  • @Ref
  • @Component (�?vue-class-component 提供)
  • Mixins (�?vue-class-component 提供)

@Component

Component装饰器它注明了此类为一个Vue组件,因此即使没有设置选项也不能省�?
如果需要定义比�?namecomponentsfiltersdirectives以及自定义属性,就可以在Component装饰器中定义,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {Component,Vue} from 'vue-property-decorator';
import {componentA,componentB} from '@/components';

@Component({
components:{
componentA,
componentB,
},
directives: {
focus: {
// 指令的定�? inserted: function (el) {
el.focus()
}
}
}
})
export default class YourCompoent extends Vue{

}

computed、data、methods

这里取消了组件的data和methods属性,以往data返回对象中的属性、methods中的方法需要直接定义在Class中,当做类的属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
export default class HelloDecorator extends Vue {
count: number = 123 // 类属性相当于以前�?data

add(): number { // 类方法就是以前的方法
this.count + 1
}

// 获取计算属�? get total(): number {
return this.count + 1
}

// 设置计算属�? set total(param:number): void {
this.count = param
}
}

@props

组件接收属性的装饰器,如下使用�?

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
import {Component,Vue,Prop} from vue-property-decorator;

@Component
export default class YourComponent extends Vue {
@Prop(String)
propA:string;

@Prop([String,Number])
propB:string|number;

@Prop({
type: String, // type: [String , Number]
default: 'default value', // 一般为String或Number
//如果是对象或数组的话。默认值从一个工厂函数中返回
// defatult: () => {
// return ['a','b']
// }
required: true,
validator: (value) => {
return [
'InProcess',
'Settled'
].indexOf(value) !== -1
}
})
propC:string;
}

@watch

实际就是Vue中的监听器,如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Vue, Component, Watch } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
@Watch('child')
onChildChanged(val: string, oldVal: string) {}

@Watch('person', { immediate: true, deep: true })
onPersonChanged1(val: Person, oldVal: Person) {}

@Watch('person')
onPersonChanged2(val: Person, oldVal: Person) {}
}

@emit

vue-property-decorator 提供�?@Emit 装饰器就是代替Vue 中的事件的触发$emit,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {Vue, Component, Emit} from 'vue-property-decorator';
@Component({})
export default class Some extends Vue{
mounted(){
this.$on('emit-todo', function(n) {
console.log(n)
})
this.emitTodo('world');
}
@Emit()
emitTodo(n: string){
console.log('hello');
}
}

�?、总结

可以看到上述typescript版本的vue class的语法与平时javascript版本使用起来还是有很大的不同,多处用到class与装饰器,但实际上本质是一致的,只有不断编写才会得心应�?

面试官:vue项目本地开发完成后部署到服务器后报404是什么原因呢�?

image.png

一、如何部�?

前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web容器指定的静态目录下即可

我们知道vue项目在构建后,是生成一系列的静态文�?

常规布署我们只需要将这个目录上传至目标服务器即可

1
// scp 上传 user为主机登录用户,host为主机外网ip, xx为web容器静态资源路�?scp dist.zip user@host:/xx/xx/xx

web容器跑起来,以nginx为例

1
2
3
4
5
6
7
8
server {
listen 80;
server_name www.xxx.com;

location / {
index /data/dist/index.html;
}
}

配置完成记得重启nginx

1
2
3
4
// 检查配置是否正�?nginx -t 

// 平滑重启
nginx -s reload

操作完后就可以在浏览器输入域名进行访问了

当然上面只是提到最简单也是最直接的一种布署方�?
什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开

二�?04问题

这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?

我们先还原一下场景:

  • vue项目在本地时运行正常,但部署到服务器中,刷新页面,出现了404错误

先定位一下,HTTP 404 错误意味着链接指向的资源不存在

问题在于为什么不存在?且为什么只有history模式下会出现这个问题�?

为什么history模式下有问题

Vue是属于单页应用(single-page application�?
SPA是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html

现在,我们回头来看一下我们的nginx配置

1
2
3
4
5
6
7
8
server {
listen 80;
server_name www.xxx.com;

location / {
index /data/dist/index.html;
}
}

可以根据 nginx 配置得出,当我们在地址栏输�?www.xxx.com 时,这时会打开我们 dist 目录下的 index.html 文件,然后我们在跳转路由进入�?www.xxx.com/login

关键在这里,当我们在 website.com/login 页执行刷新操作,nginx location 是没有相关配置的,所以就会出�?404 的情�?

为什么hash模式下没有问�?

router hash 模式我们都知道是用符�?表示的,�? website.com/#/login, hash 的值为 #/login

它的特点在于:hash 虽然出现�?URL 中,但不会被包括�?HTTP 请求中,对服务端完全没有影响,因此改�?hash 不会重新加载页面

hash 模式下,�?hash 符号之前的内容会被包含在请求中,�?website.com/#/login 只有 website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误

解决方案

看到这里我相信大部分同学都能想到怎么解决问题了,

产生问题的本质是因为我们的路由是通过JS来执行视图切换的�?
当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出�?04

所以我们只需要配置将任意页面都重定向�?index.html,把路由交由前端处理

nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name www.xxx.com;

location / {
index /data/dist/index.html;
try_files $uri $uri/ /index.html;
}
}

修改完配置文件后记得配置的更�?

1
nginx -s reload

这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返�?index.html 文件

为了避免这种情况,你应该�?Vue 应用里面覆盖所有的路由情况,然后在给出一�?404 页面

1
2
3
4
5
6
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '*', component: NotFoundComponent }
]
})

关于后端配置方案还有:Apachenodejs等,思想是一致的,这里就不展开述说�?

参考文�?

面试官:Vue项目中有封装过axios吗?主要是封装哪方面的?

一、axios是什�?

axios 是一个轻量的 HTTP客户�?
基于 XMLHttpRequest 服务来执�?HTTP 请求,支持丰富的配置,支�?Promise,支持浏览器端和 Node.js 端。自Vue2.0起,尤大宣布取消�?vue-resource 的官方推荐,转而推�?axios。现�?axios 已经成为大部�?Vue 开发者的首�?

特�?

  • 从浏览器中创�?XMLHttpRequests
  • �?node.js 创建 http请求
  • 支持 Promise API
  • 拦截请求和响�?- 转换请求数据和响应数�?- 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御XSRF

基本使用

安装

1
2
3
// 项目中安�?npm install axios --S
// cdn 引入
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

导入

1
import axios from 'axios'

发送请�?

1
2
3
4
5
6
7
8
9
10
11
axios({        
url:'xxx', // 设置请求的地址
method:"GET", // 设置请求方法
params:{ // get请求使用params进行参数凭�?如果是post请求用data
type: '',
page: 1
}
}).then(res => {
// res为后端返回的数据
console.log(res);
})

并发请求axios.all([])

1
2
3
4
5
6
7
8
9
10
11
12
function getUserAccount() {
return axios.get('/user/12345');
}

function getUserPermissions() {
return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (res1, res2) {
// res1第一个请求的返回的内容,res2第二个请求返回的内容
// 两个请求都执行完成才会执�?}));

二、为什么要封装

axios �?API 很友好,你完全可以很轻松地在项目中直接使用�?
不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一�?
这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一�?axios 再使�?
举个例子�?

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
axios('http://localhost:3000/data', {
// 配置代码
method: 'GET',
timeout: 1000,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
Authorization: 'xxx',
},
transformRequest: [function (data, headers) {
return data;
}],
// 其他请求配置...
})
.then((data) => {
// todo: 真正业务逻辑代码
console.log(data);
}, (err) => {
// 错误处理代码
if (err.response.status === 401) {
// handle authorization error
}
if (err.response.status === 403) {
// handle server forbidden error
}
// 其他错误处理.....
console.log(err);
});

如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了

这时候我们就需要对axios进行二次封装,让使用更为便利

三、如何封�?

封装的同时,你需要和 后端协商好一些约定,请求头,状态码,请求超时时�?……

设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区�?
请求�?: 来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业�?

状态码: 根据接口返回的不同status �?来执行不同的业务,这块需要和后端约定�?
请求方法:根据getpost等方法进行一个再次封装,使用起来更为方便

请求拦截�? 根据请求的请求头设定,来决定哪些请求可以访问

响应拦截器: 这块就是根据 后端`返回来的状态码判定执行不同业务

设置接口请求前缀

利用node环境变量来作判断,用来区分开发、测试、生产环�?

1
2
3
4
5
if (process.env.NODE_ENV === 'development') {
axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'http://prod.xxx.com'
}

在本地调试的时候,还需要在vue.config.js文件中配置devServer实现代理转发,从而实现跨�?

1
2
3
4
5
6
7
8
9
10
11
devServer: {
proxy: {
'/proxyApi': {
target: 'http://dev.xxx.com',
changeOrigin: true,
pathRewrite: {
'/proxyApi': ''
}
}
}
}

设置请求头与超时时间

大部分情况下,请求头都是固定的,只有少部分情况下,会需要一些特殊的请求头,这里将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置

1
2
3
4
5
6
7
8
9
10
11
12
const service = axios.create({
...
timeout: 30000, // 请求 30s 超时
headers: {
get: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进�? },
post: {
'Content-Type': 'application/json;charset=utf-8'
// 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进�? }
},
})

封装请求方法

先引入封装好的方法,在要调用的接口重新封装成一个方法暴露出�?

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
// get 请求
export function httpGet({
url,
params = {}
}) {
return new Promise((resolve, reject) => {
axios.get(url, {
params
}).then((res) => {
resolve(res.data)
}).catch(err => {
reject(err)
})
})
}

// post
// post请求
export function httpPost({
url,
data = {},
params = {}
}) {
return new Promise((resolve, reject) => {
axios({
url,
method: 'post',
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}],
// 发送的数据
data,
// url参数
params

}).then(res => {
resolve(res.data)
})
})
}

把封装的方法放在一个api.js文件�?

1
2
import { httpGet, httpPost } from './http'
export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params })

页面中就能直接调�?

1
2
3
4
5
6
// .vue
import { getorglist } from '@/assets/js/api'

getorglist({ id: 200 }).then(res => {
console.log(res)
})

这样可以把api统一管理起来,以后维护修改只需要在api.js文件操作即可

请求拦截�?

请求拦截器可以在每个请求里加上token,做了统一处理后维护起来也方便

1
2
3
4
5
6
7
8
9
10
// 请求拦截�?axios.interceptors.request.use(
config => {
// 每次发送请求之前判断是否存在token
// 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的
token && (config.headers.Authorization = token)
return config
},
error => {
return Promise.error(error)
})

响应拦截�?

响应拦截器可以在接收到响应后先做一层操作,如根据状态码判断登录状态、授�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 响应拦截�?axios.interceptors.response.use(response => {
// 如果返回的状态码�?00,说明接口请求成功,可以正常拿到数据
// 否则的话抛出错误
if (response.status === 200) {
if (response.data.code === 511) {
// 未授权调取授权接�? } else if (response.data.code === 510) {
// 未登录跳转登录页
} else {
return Promise.resolve(response)
}
} else {
return Promise.reject(response)
}
}, error => {
// 我们可以在这里对异常状态作统一处理
if (error.response.status) {
// 处理请求失败的情�? // 对不同返回码对相应处�? return Promise.reject(error.response)
}
})

小结

  • 封装是编程中很有意义的手段,简单的axios封装,就可以让我们可以领略到它的魅力
  • 封装 axios 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方�?

参考文�?

面试官:你了解axios的原理吗?有看过它的源码吗?

一、axios的使�?

关于axios的基本使用,上篇文章已经有所涉及,这里再稍微回顾下:

*发送请�?

1
2
3
4
5
6
7
8
9
10
import axios from 'axios';

axios(config) // 直接传入配置
axios(url[, config]) // 传入url和配�?axios[method](url[, option]) // 直接调用请求方式方法,传入url和配�?axios[method](url[, data[, option]]) // 直接调用请求方式方法,传入data、url和配�?axios.request(option) // 调用 request 方法

const axiosInstance = axios.create(config)
// axiosInstance 也具有以�?axios 的能�?
axios.all([axiosInstance1, axiosInstance2]).then(axios.spread(response1, response2))
// 调用 all 和传�?spread 回调

*请求拦截�?

1
2
3
4
5
6
axios.interceptors.request.use(function (config) {
// 这里写发送请求前处理的代�? return config;
}, function (error) {
// 这里写发送请求错误相关的代码
return Promise.reject(error);
});

*响应拦截�?

1
2
3
4
5
6
axios.interceptors.response.use(function (response) {
// 这里写得到响应数据后处理的代�? return response;
}, function (error) {
// 这里写得到错误响应处理的代码
return Promise.reject(error);
});

取消请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方式一
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('xxxx', {
cancelToken: source.token
})
// 取消请求 (请求原因是可选的)
source.cancel('主动取消请求');

// 方式�?const CancelToken = axios.CancelToken;
let cancel;

axios.get('xxxx', {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
});
cancel('主动取消请求');

二、实现一个简易版axios

构建一个Axios构造函数,核心代码为request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Axios {
constructor() {

}

request(config) {
return new Promise(resolve => {
const {url = '', method = 'get', data = {}} = config;
// 发送ajax请求
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onload = function() {
console.log(xhr.responseText)
resolve(xhr.responseText);
}
xhr.send(data);
})
}
}

导出axios实例

1
2
3
4
5
6
7
8
9
// 最终导出axios的方法,即实例的request方法
function CreateAxiosFn() {
let axios = new Axios();
let req = axios.request.bind(axios);
return req;
}

// 得到最后的全局变量axios
let axios = CreateAxiosFn();

上述就已经能够实现axios({ })这种方式的请�?
下面是来实现下axios.method()这种形式的请�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义get,post...方法,挂在到Axios原型�?const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
methodsArr.forEach(met => {
Axios.prototype[met] = function() {
console.log('执行'+met+'方法');
// 处理单个方法
if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参�?url[, config])
return this.request({
method: met,
url: arguments[0],
...arguments[1] || {}
})
} else { // 3个参�?url[,data[,config]])
return this.request({
method: met,
url: arguments[0],
data: arguments[1] || {},
...arguments[2] || {}
})
}

}
})

Axios.prototype上的方法搬运到request�?
首先实现个工具类,实现将b方法混入到a,并且修改this指向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const utils = {
extend(a,b, context) {
for(let key in b) {
if (b.hasOwnProperty(key)) {
if (typeof b[key] === 'function') {
a[key] = b[key].bind(context);
} else {
a[key] = b[key]
}
}

}
}
}

修改导出的方�?

1
2
3
4
5
6
7
8
9
function CreateAxiosFn() {
let axios = new Axios();

let req = axios.request.bind(axios);
// 增加代码
utils.extend(req, Axios.prototype, axios)

return req;
}

构建拦截器的构造函�?

1
2
3
4
5
6
7
8
9
10
11
12
class InterceptorsManage {
constructor() {
this.handlers = [];
}

use(fullfield, rejected) {
this.handlers.push({
fullfield,
rejected
})
}
}

实现axios.interceptors.response.useaxios.interceptors.request.use

1
2
3
4
5
6
7
8
9
10
11
12
13
class Axios {
constructor() {
// 新增代码
this.interceptors = {
request: new InterceptorsManage,
response: new InterceptorsManage
}
}

request(config) {
...
}
}

执行语句axios.interceptors.response.useaxios.interceptors.request.use的时候,实现获取axios实例上的interceptors对象,然后再获取responserequest拦截器,再执行对应的拦截器的use方法

Axios上的方法和属性搬到request过去

1
2
3
4
5
6
7
8
9
10
function CreateAxiosFn() {
let axios = new Axios();

let req = axios.request.bind(axios);
// 混入方法�?处理axios的request方法,使之拥有get,post...方法
utils.extend(req, Axios.prototype, axios)
// 新增代码
utils.extend(req, axios)
return req;
}

现在request也有了interceptors对象,在发送请求的时候,会先获取request拦截器的handlers的方法来执行

首先将执行ajax的请求封装成一个方�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
request(config) {
this.sendAjax(config)
}
sendAjax(config){
return new Promise(resolve => {
const {url = '', method = 'get', data = {}} = config;
// 发送ajax请求
console.log(config);
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onload = function() {
console.log(xhr.responseText)
resolve(xhr.responseText);
};
xhr.send(data);
})
}

获得handlers中的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
request(config) {
// 拦截器和请求组装队列
let chain = [this.sendAjax.bind(this), undefined] // 成对出现的,失败回调暂时不处�?
// 请求拦截
this.interceptors.request.handlers.forEach(interceptor => {
chain.unshift(interceptor.fullfield, interceptor.rejected)
})

// 响应拦截
this.interceptors.response.handlers.forEach(interceptor => {
chain.push(interceptor.fullfield, interceptor.rejected)
})

// 执行队列,每次执行一对,并给promise赋最新的�? let promise = Promise.resolve(config);
while(chain.length > 0) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise;
}

chains大概是['fulfilled1','reject1','fulfilled2','reject2','this.sendAjax','undefined','fulfilled2','reject2','fulfilled1','reject1']这种形式

这样就能够成功实现一个简易版axios

三、源码分�?

首先看看目录结构

axios发送请求有很多实现的方法,实现入口文件为axios.js

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
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);

// instance指向了request方法,且上下文指向context,所以可以直接以 instance(option) 方式调用
// Axios.prototype.request 内对第一个参数的数据类型判断,使我们能够�?instance(url, option) 方式调用
var instance = bind(Axios.prototype.request, context);

// 把Axios.prototype上的方法扩展到instance对象上,
// 并指定上下文为context,这样执行Axios原型链上的方法时,this会指向context
utils.extend(instance, Axios.prototype, context);

// Copy context to instance
// 把context对象上的自身属性和方法扩展到instance�? // 注:因为extend内部使用的forEach方法对对象做for in 遍历时,只遍历对象本身的属性,而不会遍历原型链上的属�? // 这样,instance 就有�? defaults、interceptors 属性�? utils.extend(instance, context);
return instance;
}

// Create the default instance to be exported 创建一个由默认配置生成的axios实例
var axios = createInstance(defaults);

// Factory for creating new instances 扩展axios.create工厂函数,内部也�?createInstance
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};

axios.spread = function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};
module.exports = axios;

主要核心�?Axios.prototype.request,各种请求方式的调用实现都是�?request 内部实现的, 简单看�?request 的逻辑

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
Axios.prototype.request = function request(config) {
// Allow for axios('example/url'[, config]) a la fetch API
// 判断 config 参数是否�?字符串,如果是则认为第一个参数是 URL,第二个参数是真正的config
if (typeof config === 'string') {
config = arguments[1] || {};
// �?url 放置�?config 对象中,便于之后�?mergeConfig
config.url = arguments[0];
} else {
// 如果 config 参数是否�?字符串,则整体都当做config
config = config || {};
}
// 合并默认配置和传入的配置
config = mergeConfig(this.defaults, config);
// 设置请求方法
config.method = config.method ? config.method.toLowerCase() : 'get';
/*
something... 此部分会在后续拦截器单独讲述
*/
};

// �?Axios 原型上挂�?'delete', 'get', 'head', 'options' 且不传参的请求方法,实现内部也是 request
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});

// �?Axios 原型上挂�?'post', 'put', 'patch' 且传参的请求方法,实现内部同样也�?request
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});

request入口参数为config,可以说config贯彻了axios的一�?
axios 中的 config 主要分布在这几个地方�?

  • 默认配置 defaults.js
  • config.method默认�?get
  • 调用 createInstance 方法创建 axios 实例,传入的config
  • 直接或间接调�?request 方法,传入的 config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// axios.js
// 创建一个由默认配置生成的axios实例
var axios = createInstance(defaults);

// 扩展axios.create工厂函数,内部也�?createInstance
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Axios.js
// 合并默认配置和传入的配置
config = mergeConfig(this.defaults, config);
// 设置请求方法
config.method = config.method ? config.method.toLowerCase() : 'get';

从源码中,可以看到优先级:默认配置对象default < method:get < Axios的实例属性this.default < request参数

下面重点看看request方法

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
Axios.prototype.request = function request(config) {
/*
先是 mergeConfig ... 等,不再阐述
*/
// Hook up interceptors middleware 创建拦截器链. dispatchRequest 是重中之重,后续重点
var chain = [dispatchRequest, undefined];

// push各个拦截器方�?注意:interceptor.fulfilled �?interceptor.rejected 是可能为undefined
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 请求拦截器逆序 注意此处�?forEach 是自定义的拦截器的forEach方法
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 响应拦截器顺�?注意此处�?forEach 是自定义的拦截器的forEach方法
chain.push(interceptor.fulfilled, interceptor.rejected);
});

// 初始化一个promise对象,状态为resolved,接收到的参数为已经处理合并过的config对象
var promise = Promise.resolve(config);

// 循环拦截器的�? while (chain.length) {
promise = promise.then(chain.shift(), chain.shift()); // 每一次向外弹出拦截器
}
// 返回 promise
return promise;
};

拦截器interceptors是在构建axios实例化的属�?

1
2
3
4
5
6
7
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(), // 请求拦截
response: new InterceptorManager() // 响应拦截
};
}

InterceptorManager构造函�?

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
// 拦截器的初始�?其实就是一组钩子函�?function InterceptorManager() {
this.handlers = [];
}

// 调用拦截器实例的use时就是往钩子函数中push方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};

// 拦截器是可以取消的,根据use的时候返回的ID,把某一个拦截器方法置为null
// 不能�?splice 或�?slice 的原因是 删除之后 id 就会变化,导致之后的顺序或者是操作不可�?InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};

// 这就是在 Axios的request方法�?中循环拦截器的方�?forEach 循环执行钩子函数
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
}

请求拦截器方法是�?unshift到拦截器中,响应拦截器是被push到拦截器中的。最终它们会拼接上一个叫dispatchRequest的方法被后续�?promise 顺序执行

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');

// 判断请求是否已被取消,如果已经被取消,抛出已取消
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}

module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);

// 如果包含baseUrl, 并且不是config.url绝对路径,组合baseUrl以及config.url
if (config.baseURL && !isAbsoluteURL(config.url)) {
// 组合baseURL与url形成完整的请求路�? config.url = combineURLs(config.baseURL, config.url);
}

config.headers = config.headers || {};

// 使用/lib/defaults.js中的transformRequest方法,对config.headers和config.data进行格式�? // 比如将headers中的Accept,Content-Type统一处理成大�? // 比如如果请求正文是一个Object会格式化为JSON字符串,并添加application/json;charset=utf-8的Content-Type
// 等一系列操作
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);

// 合并不同配置的headers,config.headers的配置优先级更高
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers || {}
);

// 删除headers中的method属�? utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);

// 如果config配置了adapter,使用config中配置adapter的替代默认的请求方法
var adapter = config.adapter || defaults.adapter;

// 使用adapter方法发起请求(adapter根据浏览器环境或者Node环境会有不同�? return adapter(config).then(
// 请求正确返回的回�? function onAdapterResolution(response) {
// 判断是否以及取消了请求,如果取消了请求抛出以取消
throwIfCancellationRequested(config);

// 使用/lib/defaults.js中的transformResponse方法,对服务器返回的数据进行格式�? // 例如,使用JSON.parse对响应正文进行解�? response.data = transformData(
response.data,
response.headers,
config.transformResponse
);

return response;
},
// 请求失败的回�? function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);

if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
}
);
};

再来看看axios是如何实现取消请求的,实现文件在CancelToken.js

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
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
// �?CancelToken 上定义一�?pending 状态的 promise ,将 resolve 回调赋值给外部变量 resolvePromise
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});

var token = this;
// 立即执行 传入�?executor函数,将真实�?cancel 方法通过参数传递出去�? // 一旦调用就执行 resolvePromise 即前面的 promise �?resolve,就更改promise的状态为 resolve�? // 那么xhr中定义的 CancelToken.promise.then方法就会执行, 从而xhr内部会取消请�? executor(function cancel(message) {
// 判断请求是否已经取消过,避免多次执行
if (token.reason) {
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}

CancelToken.source = function source() {
// source 方法就是返回了一�?CancelToken 实例,与直接使用 new CancelToken 是一样的操作
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
// 返回创建�?CancelToken 实例以及取消方法
return {
token: token,
cancel: cancel
};
};

实际上取消请求的操作是在 xhr.js 中也有响应的配合�?

1
2
3
4
5
6
7
8
9
10
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
// 取消请求
request.abort();
reject(cancel);
});
}

巧妙的地方在 CancelToken�?executor 函数,通过resolve函数的传递与执行,控制promise的状�?

小结

参考文�?

面试官:Vue组件之间的通信方式都有哪些�?

一、组件间通信的概�?

开始之前,我们�?*组件间通信**这个词进行拆�?

  • 组件
  • 通信

都知道组件是vue最强大的功能之一,vue中每一个.vue我们都可以视之为一个组件通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的。广义上,任何信息的交通都是通信组件间通信即指组件(.vue)通过某种方式来传递信息以达到某个目的举个栗子我们在使用UI框架中的table组件,可能会往table组件中传入某些数据,这个本质就形成了组件之间的通信

二、组件间通信解决了什�?

在古代,人们通过驿站、飞鸽传书、烽火报警、符号、语言、眼神、触碰等方式进行信息传递,到了今天,随着科技水平的飞速发展,通信基本完全利用有线或无线电完成,相继出现了有线电话、固定电话、无线电话、手机、互联网甚至视频电话等各种通信方式从上面这段话,我们可以看到通信的本质是信息同步,共享回到vue中,每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统

二、组件间通信的分�?

组件间通信的分类可以分成以�?

  • 父子组件之间的通信
  • 兄弟组件之间的通信
  • 祖孙与后代组件之间的通信
  • 非关系组件间之间的通信

关系�?

三、组件间通信的方�?

整理vue�?种常规的通信方案

  1. 通过 props 传�?2. 通过 $emit 触发自定义事�?3. 使用 ref
  2. EventBus
  3. $parent �?root
  4. attrs �?listeners
  5. Provide �?Inject
  6. Vuex

props传递数�?

  • 适用场景:父组件传递数据给子组�?- 子组件设置props属性,定义接收父组件传递过来的参数
  • 父组件在使用子组件标签中通过字面量来传递�?
    Children.vue
1
2
3
4
5
6
7
8
9
10
props:{  
    // 字符串形�?
 name:String // 接收的类型参�?
    // 对象形式
    age:{  
        type:Number// 接收的类型为数�?
        defaule:18,  // 默认值为18
       require:true // age属性必须传�?
    }
}

Father.vue组件

1
<Children name="jack" age=18 />  

$emit 触发自定义事�?

  • 适用场景:子组件传递数据给父组�?- 子组件通过$emit触发自定义事件,$emit第二个参数为传递的数�?- 父组件绑定监听器获取到子组件传递过来的参数

Chilfen.vue

1
this.$emit('add', good)  

Father.vue

1
<Children @add="cartAdd($event)" />  

ref

  • 父组件在使用子组件的时候设置ref
  • 父组件通过设置子组件ref来获取数�?
    父组�?
    1
    2
    3
    <Children ref="foo" />  

    this.$refs.foo  // 获取子组件实例,通过子组件实例我们就能拿到对应的数据

EventBus

  • 使用场景:兄弟组件传�?- 创建一个中央事件总线EventBus
  • 兄弟组件通过$emit触发自定义事件,$emit第二个参数为传递的数�?- 另一个兄弟组件通过$on监听自定义事�?
    Bus.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建一个中央时间总线�? 
class Bus {
  constructor() {
    this.callbacks = {};   // 存放事件的名�?
  }
  $on(name, fn) {
    this.callbacks[name] = this.callbacks[name] || [];
    this.callbacks[name].push(fn);
  }
  $emit(name, args) {
    if (this.callbacks[name]) {
      this.callbacks[name].forEach((cb) => cb(args));
    }
  }
}

// main.js
Vue.prototype.$bus = new Bus() // �?bus挂载到vue实例的原型上
// 另一种方�?
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功�?

Children1.vue

1
this.$bus.$emit('foo')  

Children2.vue

1
this.$bus.$on('foo'this.handle)  

$parent �? root

  • 通过共同祖辈$parent或者$root搭建通信桥连

兄弟组件

this.$parent.on('add',this.add)

另一个兄弟组�?
this.$parent.emit('add')

$attrs �? listeners

  • 适用场景:祖先传递数据给子孙
  • 设置批量向下传属性$attrs�?$listeners
  • 包含了父级作用域中不作为 prop 被识�?(且获取) 的特性绑�?( class �?style 除外)�? - 可以通过 v-bind="$attrs" 传⼊内部组件
1
2
3
4
5
// child:并未在props中声明foo  
<p>{{$attrs.foo}}</p>

// parent
<HelloWorld foo="foo"/>
1
2
3
4
5
6
7
8
9
10
// 给Grandson隔代传值,communication/index.vue  
<Child2 msg="lalala" @some-event="onSomeEvent"></Child2>

// Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>

// Grandson使⽤
<div @click="$emit('some-event', 'msg from grandson')">
{{msg}}
</div>

provide �?inject

  • 在祖先组件定义provide属性,返回传递的�?- 在后代组件通过inject接收组件传递过来的�?
    祖先组件
1
2
3
4
5
provide(){  
    return {
        foo:'foo'
    }
}

后代组件

1
inject:['foo'// 获取到祖先组件传递过来的�? 

vuex

  • 适用场景: 复杂关系的组件数据传�?- Vuex作用相当于一个用来存储共享变量的容器

  • state用来存放共享变量的地�?- getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的�?- mutations用来存放修改state的方法�?- actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操�?

小结

  • 父子关系的组件数据传递选择 props  �?$emit进行传递,也可选择ref
  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传�?- 祖先与后代组件数据传递可选择attrslisteners或�?Provide�?Inject
  • 复杂关系的组件数据传递可以通过vuex存放共享的变�?

参考文�?

面试官VUE系列总进度:5�?3

面试官:说说你对vue的理解?

面试官:说说你对SPA(单页应用)的理解?

面试官:说说你对双向绑定的理解?

面试官:说说你对Vue生命周期的理解?

面试官:双向数据绑定是什�?

一、什么是双向绑定

我们先从单向绑定切入单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了ViewModel的数据也自动被更新了,这种情况就是双向绑定举个栗�?

当用户填写表单时,View的状态就被更新了,如果此时可以自动更新Model的状态,那就相当于我们把ModelView做了双向绑定关系图如�?

二、双向绑定的原理是什�?

我们都知�?Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起�?
    而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便�?“数据双向绑定�?。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解ViewModel

它的主要职责就是�?

  • 数据变化后更新视�?- 视图变化后更新数�?
    当然,它还有两个主要部分组成

  • 监听器(Observer):对所有数据的属性进行监�?- 解析器(Compiler):对每个元素节点的指令进行扫描跟解�?根据指令模板替换数据,以及绑定相应的更新函�?

三、实现双向绑�?

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe�?2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile�?3. 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函�?4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
  2. 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

流程图如下:

实现

先来一个构造函数:执行初始化,对data执行响应化处�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Vue {  
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
      
    // 对data选项做响应式处理
    observe(this.$data);
      
    // 代理data到vm�?
    proxy(this);
      
    // 执行编译
    new Compile(options.elthis);
  }
}

data选项执行响应化具体操�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function observe(obj) {  
  if (typeof obj !== "object" || obj == null) {
    return;
  }
  new Observer(obj);
}

class Observer {
  constructor(value) {
    this.value = value;
    this.walk(value);
  }
  walk(obj) {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

编译Compile

对每个元素节点的指令进行扫描跟解�?根据指令模板替换数据,以及绑定相应的更新函�?

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
class Compile {  
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);  // 获取dom
    if (this.$el) {
      this.compile(this.$el);
    }
  }
  compile(el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach((node) => { // 遍历子元�?
      if (this.isElement(node)) {   // 判断是否为节�?
        console.log("编译元素" + node.nodeName);
      } else if (this.isInterpolation(node)) {
        console.log("编译插值⽂�? + node.textContent);  // 判断是否为插值文本 {{}}
      }
      if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素
        this.compile(node);  // 对子元素进行递归遍历
      }
    });
  }
  isElement(node) {
    return node.nodeType == 1;
  }
  isInterpolation(node) {
    return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
}

依赖收集

视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知

实现思路

  1. defineReactive时为每⼀个key创建⼀个Dep实例
  2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1
  3. 由于触发name1getter方法,便将watcher1添加到name1对应的Dep�? 4. 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 负责更新视图  
class Watcher {
  constructor(vm, key, updater) {
    this.vm = vm
    this.key = key
    this.updaterFn = updater

    // 创建实例时,把当前实例指定到Dep.target静态属性上
    Dep.target = this
    // 读一下key,触发get
    vm[key]
    // 置空
    Dep.target = null
  }

  // 未来执行dom更新函数,由dep调用�?
  update() {
    this.updaterFn.call(this.vmthis.vm[this.key])
  }
}

声明Dep

1
2
3
4
5
6
7
8
9
10
11
class Dep {  
  constructor() {
    this.deps = [];  // 依赖管理
  }
  addDep(dep) {
    this.deps.push(dep);
  }
  notify() { 
    this.deps.forEach((dep) => dep.update());
  }
}

创建watcher时触发getter

1
2
3
4
5
6
7
8
class Watcher {  
  constructor(vm, key, updateFn) {
    Dep.target = this;
    this.vm[this.key];
    Dep.target = null;
  }
}

依赖收集,创建Dep实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function defineReactive(obj, key, val) {  
  this.observe(val);
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      dep.notify(); // 通知dep执行更新方法
    },
  });
}

参考文�?

面试官VUE系列总进度:3�?3

面试官:说说你对vue的理解?
面试官:说说你对SPA(单页应用)的理解?

面试官:Vue中组件和插件有什么区别?

image.png

一、组件是什�?

回顾以前对组件的定义�?
组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组�?
组件的优�?

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现

  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简�?

  • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

二、插件是什�?

插件通常用来�?Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种�?

  • 添加全局方法或者属性。如: vue-custom-element
  • 添加全局资源:指�?过滤�?过渡等。如 vue-touch
  • 通过全局混入来添加一些组件选项。如 vue-router
  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现�?- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

三、两者的区别

两者的区别主要表现在以下几个方面:

  • 编写形式
  • 注册形式
  • 使用场景

编写形式

编写组件

编写一个组件,可以有很多方式,我们最常见的就是vue单文件的这种格式,每一个.vue文件我们都可以看成是一个组�?
vue文件标准格式

1
2
3
4
5
6
7
8
9
<template>
</template>
<script>
export default{
...
}
</script>
<style>
</style>

我们还可以通过template属性来编写一个组件,如果组件内容多,我们可以在外部定义template组件内容,如果组件内容并不多,我们可直接写在template属性上

1
2
3
4
5
6
7
<template id="testComponent">     // 组件显示的内�?    <div>component!</div>   
</template>

Vue.component('componentA',{
template: '#testComponent'
template: `<div>component</div>` // 组件内容少可以通过这种形式
})

编写插件

vue插件的实现应该暴露一�?install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象

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
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法�?property
Vue.myGlobalMethod = function () {
// 逻辑...
}

// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})

// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})

// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}

注册形式

组件注册

vue组件注册主要分为全局注册与局部注�?
全局注册通过Vue.component方法,第一个参数为组件的名称,第二个参数为传入的配置项

1
Vue.component('my-component-name', { /* ... */ })

局部注册只需在用到的地方通过components属性注册一个组�?

1
2
3
4
5
const component1 = {...} // 定义一个组�?
export default {
components:{
component1 // 局部注�? }
}

插件注册

插件的注册通过Vue.use()的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项

1
Vue.use(插件名字,{ /* ... */} )

注意的是�?
注册插件的时候,需要在调用 new Vue() 启动应用之前完成

Vue.use会自动阻止多次注册相同插件,只会注册一�?

使用场景

具体的其实在插件是什么章节已经表述了,这里在总结一�?
组件 (Component) 是用来构成你�?App 的业务模块,它的目标�?App.vue

插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标�?Vue 本身

简单来说,插件就是指对Vue的功能的增强或补�?

参考文�?

面试官:为什么data属性是一个函数而不是一个对象?

一、实例和组件定义data的区�?

vue实例的时候定义data属性既可以是一个对象,也可以是一个函�?

1
2
3
4
5
6
7
8
9
10
11
12
13
const app = new Vue({
el:"#app",
// 对象格式
data:{
foo:"foo"
},
// 函数格式
data(){
return {
foo:"foo"
}
}
})

组件中定义data属性,只能是一个函�?
如果为组件data直接定义为一个对�?

1
2
3
4
5
6
Vue.component('component1',{
template:`<div>组件</div>`,
data:{
foo:"foo"
}
})

则会得到警告信息

警告说明:返回的data应该是一个函数在每一个组件实例中

二、组件data定义函数与对象的区别

上面讲到组件data必须是一个函数,不知道大家有没有思考过这是为什么呢�?
在我们定义好一个组件的时候,vue最终都会通过Vue.extend()构成组件实例

这里我们模仿组件构造函数,定义data属性,采用对象的形�?

1
2
3
4
5
6
function Component(){

}
Component.prototype.data = {
count : 0
}

创建两个组件实例

1
2
const componentA = new Component()
const componentB = new Component()

修改componentA组件data属性的值,componentB中的值也发生了改�?

1
2
3
console.log(componentB.data.count)  // 0
componentA.data.count = 1
console.log(componentB.data.count) // 1

产生这样的原因这是两者共用了同一个内存地址,componentA修改的内容,同样对componentB产生了影�?
如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同�?

1
2
3
4
5
6
7
8
function Component(){
this.data = this.data()
}
Component.prototype.data = function (){
return {
count : 0
}
}

修改componentA组件data属性的值,componentB中的值不受影�?

1
2
3
console.log(componentB.data.count)  // 0
componentA.data.count = 1
console.log(componentB.data.count) // 0

vue组件可能会有很多个实例,采用函数返回一个全新data形式,使每个实例对象的数据不会受到其他实例对象数据的污染

三、原理分�?

首先可以看看vue初始化data的代码,data的定义可以是函数也可以是对象

源码位置:/vue-dev/src/core/instance/state.js

1
2
3
4
5
6
7
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
...
}

data既能是object也能是function,那为什么还会出现上文警告呢�?
别急,继续看下�?
组件在创建的时候,会进行选项的合�?
源码位置:/vue-dev/src/core/util/options.js

自定义组件会进入mergeOptions进行选项合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.prototype._init = function (options?: Object) {
...
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
}

定义data会进行数据校�?
源码位置:/vue-dev/src/core/instance/init.js

这时候vm实例为undefined,进入if判断,若data类型不是function,则出现警告提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== "function") {
process.env.NODE_ENV !== "production" &&
warn(
'The "data" option should be a function ' +
"that returns a per-instance value in component " +
"definitions.",
vm
);

return parentVal;
}
return mergeDataOrFn(parentVal, childVal);
}
return mergeDataOrFn(parentVal, childVal, vm);
};

四、结�?

  • 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
  • 组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象
0%