前端面试宝典

Vue原理

  • 面试官为何要考察原理,又用不到?

知其然知其所以然,各行各业通用的道理
了解原理,才能应用的更好(竞争激烈,择优录取)
大厂造轮子(有钱有资源,业务定制,技术KPI)

  • 面试中如何考察Vue原理?以何种方式?

考察重点,而不是考察细节。掌握好2/8原则。
和使用相关联的原理,例如:vdom、模板渲染
整体流程是否全面?热门技术是否有深度?

响应式原理(数据驱动视图)

  • 核心API,Object.defineProperty(vue3使用Proxy,但是proxy兼容性不好,且无法polyfill)

Object.defineProperty实现响应式

  • 监听对象,监听数组
  • 复杂对象,深度监听
// 更新视图方法
function updateView(){
	console.log('视图更新');
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype;

// 创建新对象,原型指向oldArrayProperty,再扩展新方法不会影响原型
const arrProto = Object.create(oldArrayProperty);

['push','pop','shift','unshift','splice'].forEach(methodName=>{
	arrProto[methodName] = function(){
		// 更新视图
		updateView()
		
		// 调用原生方法
		oldArrayProperty[methodName].call(this, ...arguments)
		// Array.prototype.push.call(this, ...arguments);
	}
})

// 重新定义属性,监听起来
function defineReactive(target, key, value){
    // 深度监听
    observer(value);
    
	// 核心API
	Object.defineProperty(target, key, {
		get(){
			return value
		},
		set(newValue){
			if(newValue !== value){
				// 深度监听
				observer(newValue)
			
				// 设置新值
				// 注意value一直在闭包中,此处设置完之后,再get时获取的是最新的值
				value = newValue
				
				// 触发更新视图
				updateView()
			}
		}
	})
}

// 监听对象属性
function observer(target){
	if(typeof target !== 'object' || target === null){
		// 不是对象或数组
		return target;
	}
	
	// 深度监听数组
	if(Array.isArray(target)){
		target.__proto__ = arrProto
	}
	
	// 重新定义各个属性(for in 也可以遍历数组)
	for(let key in target){
		defineReactive(target, key, target[key])
	}
}



const data = {
	name: 'krik',
	age: 18,
	info: {
		address: '豫'
	},
	nums: [10,20,30]
}

// 监听数据
observer(data)

// 测试
data.name = 'ime'
data.age = 19


data.x = '100' // 新增属性,监听不到,所以有Vue.set
delete data.name // 删除属性,监听不到,所以有Vue.delete
data.info.address = '上海' // 深度监听
data.nums.push(4)   // 监听数组

几个缺点:

  • 深度监听,需要递归到底,一次性计算量大
  • 无法监听新增属性/删除属性(Vue.set 和 Vue.delete)
  • 无法监听数组,需要特殊处理

虚拟DOM和diff算法

虚拟DOM库: (虚拟DOM库)[https://github.com/snabbdom/snabbdom]

  • 用JS模拟DOM结构
  • 新旧vnode对比,得出最小的更新范围,最后更新DOM
  • 数据驱动视图模式下,有效控制DOM操作

diff算法:是vdom中最核心,最关键的部分
diff算法能在日常使用vue react 中提现出来(如key)
diff算法实现代码细节

diff即对比,是一个广泛的概念,如linux diff命令,git diff等
两个js对象也可以做diff, 如https://github.com/cujojs/jiff

两棵树做diff,如这里的vdom diff

  • 树diff的时间复杂度 O(n^3)
  • 第一,遍历tree1; 第二,遍历tree2,第三,排序
  • 问题:1000个节点,要计算1亿次,算法不可用。

前端大佬针对前面的问题,提出了个解决办法:优化时间复杂度到O(n)

  • 只比较同一层级,不跨级比较
  • tag 不相同,则直接删除重建,不再深度比较
  • tag和key两者都相同,则认为是相同节点,不做深度比较

snabbdom 源码解读(细节不重要,了解流程即可):

  • patchVnode
  • addVnodes 和 removeVnodes
  • updateChildren

vdom核心概念很重要:h, vnode, patch, diff, key等
vdom存在的价值:数据驱动视图,控制DOM操作

模板编译

vue的模板不是html,有指令,插值,JS表达式,它到底是什么?

面试官不会直接问,但是会通过"组件渲染和更新过程"考察

前置知识:JS的with语法

// 使用with,能改变{}内自由变量的查找方式
// 将{}内自由变量,当做obj的属性来查找,找不到就会报错
// with 要慎用,它打破了作用域规则,易读性变差
const obj = {a: 1, b: 2}
console.log(obj.c)  // undefined
with(obj){
	console.log(a)
	console.log(b)
	console.log(c) // 会报错 !!!
}

vue template complier 将模板编译成render函数,执行render函数生成vnode
基于vnode再执行patch和diff

使用webpack vue-loader, 会在开发环境下编译模板(重要)

描述组件渲染/更新过程

  • 响应式原理(数据驱动视图)
  • 模板编译:模板到render函数,再到vnode
  • vnode diff算法

初次渲染过程=》更新过程=》异步渲染

初次渲染过程:解析模板为render函数(或在开发环境已完成,vue-loader),触发响应式,监听data属性getter和setter
执行render函数生成vnode,patch(elem,vnode)

更新过程:修改data,触发setter(此前getter中已被监听),重新执行render函数生成newVnode,patch(vnode,newValue)

前端路由原理

稍微复杂一点的SPA,都需要路由。vue-loader也是vue全家桶的标配之一。属于"和日常使用相关联的原理",面试常考。

路由模式:hash模式和history模式

// http://127.0.0.1:8081/01-hash.html?a=100&b=200#/aaa
location.protocol  // http:
location.hostname  // 127.0.0.1
location.host // 127.0.0.1:8081
location.port //8081
location.pathname // /01-hash.html
location.search // ?a=100&b=200
location.hash // #/aaa

hash 变化会触发网页跳转,即浏览器的前进、后退
hash 变化不会刷新页面,SPA必须的特点
hash 永远不会提交到server端,全权由前端去控制

前端实现hash路由

  • window.onhashchange
window.onhashchange = (event)=>{
	console.log('old url:',event.oldURL);
	console.log('new url:',event.newURL);
	
	console.log('hash:', location.hash);
}

document.addEventListener('DOMContentLoaded', ()=>{
	console.log('hash:', location.hash);
})

document.getElementById('#change-hash-btn').addEventListener('click', ()=>{
	location.href = '#/detail/id'
})

前端实现history路由

  • history.pushState
  • window.onpopstate
// 页面初次加载,获取path
document.addEventListener('DOMContentLoaded', ()=>{
	console.log('load', location.pathname);
})

// 打开一个新的路由
// 【注意】用pushState方式,浏览器不会刷新页面
document.getElementById('change-hash-btn').addEventListener('click', ()=>{
	const state = { name: 'page1' }
	console.log('切换路由到', 'page1');
	history.pushState(state, '', 'page1')
})


// 监听浏览器前进后退
window.onpopstate = (event) => {
	console.log('onpopstate', event.state, location.pathname);
}

两者选择
to B 的系统推荐使用hash,简单易用,对URL规范不敏感。
to C 的系统,可以考虑选择H5 history, 但需要服务端支持

能选择简单的,就别用复杂的,要考虑成本和收益。