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, 但需要服务端支持
能选择简单的,就别用复杂的,要考虑成本和收益。