Skip to content

34 道 Vue 面试题

<font style="background:#F8CED3;color:#70000D">Vue</font>

如何理解 MVVM 原理?

提到 MVVM,很多前端开发者都会想到 Vue 的双向绑定,然而它们并不能划等号,MVVM 是一种软件架构模式,而 Vue 只是一种在前端层面上的实现,其实不单在 Vue 里,在很多 Web 框架应用里都有相关的实现。MVVM 模式到底是什么呢?要说到 MVVM 这种模式,则必须要提及另一种大多数开发者都能耳熟能详的模式,就是 MVC 模式。

什么是 MVC?

在前几年,前后端完全分离开之前,很多很火的后端框架都会说自己是支持 MVC 模式,像 JAVA 的 SpringMVC、PHP 的 smarty、Nodejs 的 express 和 Koa,那么 MVC 的模式到底是什么样的?先看看下面这张经典的 MVC 模型图,Model(模型)、View(视图)、 Controller(控制器)相互依赖关系的三部分组成模型。1651020430045-b7e47e4a-064a-49f3-86e2-d70e5e020f00.jpeg认识一下这三部分具体是指什么。

Model

这里的 Model 在 MVC 中实际是数据模型的概念,可以把它当成从数据库里查出来后的一条数据,或者是将查询出来的元数据经过裁剪或者处理后的一个特定数据模型结构。

View

View 是视图,是将数据内容呈现给用户肉眼的界面层,View 层最终会将数据模型下的信息,渲染显示成人类能易于识别感知的部分。

Controller

Controller 是数据模型与 View 之间的桥梁层,实际界面层的各种变化都要经过它来控制,而且像用户从界面提交的数据也会经过 Controller 的组装检查生成数据模型,然后改变数据库里的数据内容。

MVC 的使用

像接触过 MVC 框架的同学就知道,如果想把数据从数据库里渲染到页面上,先要查询完数据库后,将拿到的元数据进行一些处理,一般会删掉无用的字段,或者进行多个数据模型间的数据聚合,然后再给到页面模板引擎(ejs,Thymeleaf 等)进行数据组装,最后组合完成进行渲染后生成 HTML 格式文件供浏览器展示使用。

像前面提到的各大支持 MVC 模式的 Web 开发框架,在前后端彻底分离之后就很少再提了。因为前端独立开发发布,实际相对原来的 MVC 模式是少了 View 这一层,这也让新的概念 Restful 出现在视野里,很多新的框架又开始支持提供这种前端控制轻量级模式下的适配方案。

但是前后端分离的出现后,MVC 就此没有了吗?当然不是。实际对于 MVC 模式没有特别明确的概念,在前后端分离之后可以尝试从不同的角度去看。可以理解整个系统在原先的 MVC 基础上 View 层进行细化,把整个前端项目当成一个 View 层,也可以从前端视角去看,Restful 接口返回的 Json 数据当成一个数据模型,作为 MVC 的 Model 层,而前端 Javascript 自身对数据的处理是 Contrller 层,真正的页面渲染结果是 View 层。

下面以前端视角下的 MVC 模式中举个例子,接口返回的数据 Model 模型与 View 页面之间由 Controller 连接,来完成系统中的数据展示。

html

<html>
	...
	
		
		
	
	...
</html>
<script>
	// 生成model数据模型
	function getDataApi() {
		// 模拟接口返回
		return {
			name: 'mvc',
			data: 'mvc 数据信息'
		}
	}
	
	// controller控制逻辑
	function pageController() {
		const result = getDataApi();
		document.getElementById('name').innerText = `姓名:${result.name}`;
		document.getElementById('data').innerText = result.data;
	}
</script>

什么是 MVVM?

随着前端对于控制逻辑的越来越轻量,MVVM 模式作为 MVC 模式的一种补充出现了,万变不离其宗,最终的目的都是将 Model 里的数据展示在 View 视图上,而 MVVM 相比于 MVC 则将前端开发者所要控制的逻辑做到更加符合轻量级的要求。1651020429971-5497e476-8034-4d1d-8fc1-d4e7f8d30252.png

ViewModel

在 Model 和 View 之间多了叫做 View-Model 的一层,将模型与视图做了一层绑定关系,在理想情况下,数据模型返回什么试图就应该展示什么,看看下面这个例子。

html

<html>
...
 
   
   
 
...
</html>
<script>
 // 生成model数据模型
 function getDataApi() {
   // 模拟接口返回
   return {
     name: 'mvc',
     data: 'mvc 数据信息'
  }
}

 // ViewModel控制逻辑
 function pageViewModel() {
   const result = getDataApi();
   return result;
}
</script>

上面作为理想情况下例子,在 ViewModel 引入之后,视图完全由接口返回数据驱动,由开发者所控制的逻辑非常轻量。不过这里要说明的是,在 MVVM 模式下,Controller 控制逻辑并非就没了,像操作页面 DOM 响应的逻辑被 SDK(如 Vue 的内部封装实现)统一实现了,像不操作接口返回的数据是因为服务端在数据返回给前端前已经操作好了。

例子里 pageViewModel 函数的实现是非常关键的一步,如何将数据模型与页面视图绑定起来呢?在目前的前端领域里有三类实现,Angularjs 的主动轮询检查新旧值变化更新视图、Vue 利用 ES5 的 Object.defineProperty 的 getter/setter 方法绑定、backbone 的发布订阅模式,从主动和被动的方式去实现了 ViewModel 的关系绑定,接下来主要看看 Vue 中的 MVVM 的实现。

Vue2.0 中的 MVVM 实现

1651020430045-171ce3cc-87f4-40c1-94d2-3be3257f56bc.jpegVue2.0 的 MVVM 实现中,对 View-Model 的实现本质利用的 ES5 的 Object.defineProperty方法,当Object.defineProperty 方法在给数据 Model 对象定义属性的时候先挂载一些方法,在这些方法里实现与界面的值绑定响应关系,当应用的属性被读取或者写入的时候便会触发这些方法,从而达到数据模型里的值发生变化时同步响应到页面上。

Vue 的响应式原理

html
// html
<body>
 
   {{name}}
   {{data}}
 
<body>

//js
<script src="vue.js"></script>
<script>
 // 生成model数据模型
 function getDataApi() {
   // 模拟接口返回
   return {
     name: 'mvc',
     data: 'mvc 数据信息'
  }
}
 new Vue({
   el: 'body',
   data() {
     return {
       name:'',
       data: '',
    }
  },
   mounted() {
     const result = getDataApi();
     this.name = result.name;
     this.data = result.data;
  }
})
</script>

new Vue在实例化的时候,首先将 data 方法里返回的对象属性都挂载上 setter 方法,而 setter 方法里将页面上的属性进行绑定,当页面加载时,浏览器提供的DOMContentloaded 事件触发后,调用 mounted 挂载函数,开始获取接口数据,获取完成后给 data 里属性赋值,赋值的时候触发前面挂载好的 setter 方法,从而引起页面的联动,达到响应式效果。

简易实现 Object.defineProperty 下的绑定原理

html
// html
<body>
	
	<body>
		<script>
			var data = {
				name: ''
			};
			// Data Bindings
			Object.defineProperty(data, 'name', {
				get: function(){},
				set: function(newValue){ // 页面响应处理
					document.getElementById('name').innerText = newValue
					data.name = value
				},
				enumerable: true,
				configurable: true
			});
			// 页面DOM listener
			document.getElementById('name').onchange = function(e) {
				data.name = e.target.value;
			}
</script>

实现 Vue3.0 版本的 MVVM

这里采用 Vue3.0 最新的实现方式,用 ProxyReflect来替代Object.definePropertypry的方式。先来介绍一下 ES6 里的ProxyReflect

Proxy

Proxy 是 ES6 里的新构造函数,它的作用就是代理,简单理解为有一个对象,不想完全对外暴露出去,想做一层在原对象操作前的拦截、检查、代理,这时候就要考虑 Proxy 了。

javascript
const myObj = {
	_id: '我是myObj的ID',
	name: 'mvvm',
	age: 25
}

const myProxy = new Proxy(myObj, {
	get(target, propKey) {
		if (propKey === 'age') {
			console.log('年龄很私密,禁止访问');
			return '*';
		}
		return target[propKey];
	},
	set(target, propKey, value, receiver) {
		if (propKey === '_id') {
			console.log('id无权修改');
			return;
		}
		target[propKey] = value + (receiver.time || '');
	},
	// setPrototypeOf(target, proto) {},
	// apply(target, object, args) {},
	// construct(target, args) {},
	// defineProperty(target, propKey, propDesc) {},
	// deleteProperty(target, propKey) {},
	// has(target, propKey) {},
	// ownKeys(target) {},
	// isExtensible(target) {},
	// preventExtensions(target) {},
	// getOwnPropertyDescriptor(target, propKey) {},
	// getPrototypeOf(target) {},
});

myProxy._id = 34;// id无权修改
console.log(`age is: ${myProxy.age}`);//年龄很私密,禁止访问
// age is: *myProxy.name = 'my name is Proxy';
console.log(myProxy);
// { _id: '我是myObj的ID', name: 'my name is Proxy', age: 25}
const newObj = {
	time: ` [${new Date()}]`,
};
// 原对象原型链赋值
Object.setPrototypeOf(myProxy, newObj);
myProxy.name = 'my name is newObj';
console.log(myProxy.name);
//my name is newObj [Thu Mar 19 2020 18:33:22 GMT+0800 (GMT+08:00)]

Reflect

Reflect 是 ES6 里的新的对象,非构造函数,不能用 new 操作符。可以把它跟 Math 类比,Math 是处理 JS 中数学问题的方法函数集合,Reflect是 JS 中对象操作方法函数集合,它暴露出来的方法与 Object 构造函数所带的静态方法大部分重合,实际功能也类似,Reflect的出现一部分原因是想让开发者不直接使用 Object 这一类语言层面上的方法,还有一部分原因也是为了完善一些功能。Reflect 提供的方法还有一个特点,完全与 Proxy 构造函数里 Hander 参数对象中的钩子属性一一对应。

1651020430017-a4b3afb5-5587-4591-9809-2fee195c1005.jpeg

看下面一个改变对象原型的例子。

javascript
const myObj = {
	_id: '我是myObj的ID',
	name: 'mvvm',
	age: 25
}

const myProxy = new Proxy(myObj, {
	get(target, propKey) {
		return target[propKey];
	},
	set(target, propKey, value, receiver) {
		target[propKey] = value + (receiver.time || '');
	},
	setPrototypeOf(target, proto) {
		if (proto.status === 'enable') {
			Reflect.setPrototypeOf(target, proto);
			return true;
		}
		return false;
	},
});

const newObj = {
	time: ` [${new Date()}]`,
	status: 'sable'
};
// 原对象原型链赋值
const result1 = Reflect.setPrototypeOf(myProxy, {
	time: ` [${new Date()}]`,
	status: 'disable'
});
myProxy.name = 'first set name'
console.log(result1)  //false
console.log(myProxy.name);  //first set name

// 原对象原型链赋值
const result2 = Reflect.setPrototypeOf(myProxy, {
	time: ` [${new Date()}]`,
	status: 'enable'
});

myProxy.name = 'second set name'
console.log(result1)  //true
console.log(myProxy.name); //second set name [Thu Mar 19 2020 19:43:59 GMT+0800 (GMT+08:00)]

/*当执行到这里时直接报错了*/
// 原对象原型链赋值
Object.setPrototypeOf(myProxy, {
	time: ` [${new Date()}]`,
	status: 'disable'
});
myProxy.name = 'third set name'
console.log(myProxy.name);

解释一下上面的这段代码,通过 Reflec.setPrototypeOf方法修改原对象原型时,必须经过Proxy里 hander 的挂载的setPrototypeOf挂载函数,在挂载函数里进行条件proto.status 是否是 enable 筛选后,再决定是否真正修改原对象 myObj 的原型,最后返回 true 或者 false 来告知外部原型是否修改成功。

这里还有一个关键点,就是在代码执行到原有的 Object.setPrototypeOf 方法时,程序则直接抛错,这其实也是 Reflect 出现的一个原因,即使现在 ES5 里的 Object 有同样的功能,但是 Reflect 实现的更友好,更适合开发者开发应用程序。

实现 MVVM

接下来使用上面的 Proxy 和 Reflect 来实现 MVVM,这里将 data 和 Proxy 输出到全局 Window 下,方便模拟数据双向联动的效果。

html
<!DOCTYPE html>
<html>
	
		name: <input id="name" />
		age: <input id="age" />
	
</html>
<script>
	// 与页面绑定
	const data = {
		name: '',
		age: 0
	}
	
	// 暴露到外部,便于查看效果
	window.data = data;
	window.myProxy = new Proxy(data, {
		set(target, propKey, value) {
			// 改变数据Model时修改页面
			if (propKey === 'name') {
				document.getElementById('name').value = value;
			} else if (propKey === 'age') {
				document.getElementById('age').value = value;
			}
			Reflect.set(...arguments);
		},
	});
	
	// 页面变化改变Model内数据
	document.getElementById('name').onchange = function(e) {
		Reflect.set(data, 'name', e.target.value);
	}
	document.getElementById('age').onchange = function(e) {
		Reflect.set(data, 'age', e.target.value);
	}
</script>

1651020429952-fb038af6-83dc-45c8-b6f3-18dc2e52a011.jpeg

先打印了 data,然后模拟有异步数据过来,手动修改 data 里的数据 window.myProxy.age=25,这时候页面上的 age 联动变化为 25,再次打印了查看 data。接下来在页面上手动输入 name,输入完成后触发输入框的 onchange 事件后,再次查看 data,此时 model 里的数据已经变化为最新的与页面保持一致的值。

更新: 2022-04-27 08:51:38
原文: <https://www.yuque.com/fcant/notes/cmit66&gt;

最近更新