@TOC
Vue原理(大厂必考)
面试为何会考察原理
面试中如何考察?以何种方式
考察重点,而不是考察细节,掌握好2/8原则 和使用相关联的原理,例如vdom、模板渲染 整体流程是否全面?热门技术是否有深度?
Vue原理包括哪些?
组件化 响应式 vdom和diff 模板编译 渲染过程 前端路由
如何理解MVVM
组件化基础
“很久以前”就有组件化
asp jsp php已经有组件化了
nodejs中也有类似的组件化
数据驱动视图(MVVM,setState)
传统组件,只是静态渲染,更新还要依赖于操作DOM 数据驱动视图--Vue MVVM 数据驱动视图--React setState(暂时先不看)
Vue MVVM
总结
组件化 数据驱动视图 MVVM
监听data变化的核心API是什么
Vue响应式
组件data的数据一旦变化,立刻触发视图的更新
实现数据驱动视图的第一步
考察Vue原理的第一题
核心API-Object.defineProperty
如何实现响应式,代码演示
Object.defineProperty的一些缺点(Vue3.0启用Proxy)
Proxy有兼容性问题
Proxy兼容性不好,且无法polyfill Vue2.x还会存在一段时间,所以都得学 Vue3.0相关知识,下一章将,这里只是先提一下
Object.defineProperty基本用法
Object.defineProperty实现响应式
监听对象,监听数组 复杂对象,深度监听 几个缺点
如何深度监听data变化、数组变化
Object.defineProperty缺点
深度监听,需要递归到底,一次性计算量大 无法监听新增属性/删除属性(Vue.set Vue.delete) 无法原生监听数组,需要特殊处理
// 触发更新视图
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
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
总结
基础API-Object.defineProperty 如何监听对象(深度监听),监听数组 Object.defineProperty的缺点
虚拟DOM-面试里的网红
虚拟DOM(Virtual DOM)和diff
vdom是实现vue和React的重要基石 diff算法是vdom中最核心、最关键的部分 vdom是一个热门话题,也是面试中的热门话题 DOM操作非常耗费性能 以前用jQuery,可以自行控制DOM操作的时机,手动调整 Vue和React是数据驱动视图,如何有效控制DOM操作?
解决方案-vdom
有了一定复杂度,想减少计算次数比较难
能不能把计算,更多的转移为JS计算?因为JS执行速度很快
vdom-用JS模拟DOM结构,计算出最小的变更,操作DOM
通过snabbdom学习vdom
简洁强大的vdom库,易学易用 Vue参考它实现的vdom和diff github.com/snabbdom/sn… Vue3.0重写了vdom的代码,优化了性能 但vdom的基本理念不变,面试考点也不变 React vdom具体实现和Vue也不同,但不妨碍统一学习
用过虚拟DOM吗?
snabbdom重点总结
h函数 vnode数据结构 patch函数
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script src="./demo1.js"></script>
</body>
</html>
const snabbdom = window.snabbdom
// 定义 patch
const patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义 h
const h = snabbdom.h
const container = document.getElementById('container')
// 生成 vnode
const vnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item 2')
])
patch(container, vnode)
document.getElementById('btn-change').addEventListener('click', () => {
// 生成 newVnode
const newVnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item B'),
h('li.item', {}, 'Item 3')
])
patch(vnode, newVnode)
})
//table-without-vdom.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
<script type="text/javascript">
const data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 渲染函数
function render(data) {
const $container = $('#container')
// 清空容器,重要!!!
$container.html('')
// 拼接 table
const $table = $('<table>')
$table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
data.forEach(item => {
$table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>'))
})
// 渲染到页面
$container.append($table)
}
$('#btn-change').click(() => {
data[1].age = 30
data[2].address = '深圳'
// re-render 再次渲染
render(data)
})
// 页面加载完立刻执行(初次渲染)
render(data)
</script>
</body>
</html>
//table-with-vdom.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script type="text/javascript">
const snabbdom = window.snabbdom
// 定义关键函数 patch
const patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义关键函数 h
const h = snabbdom.h
// 原始数据
const data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 把表头也放在 data 中
data.unshift({
name: '姓名',
age: '年龄',
address: '地址'
})
const container = document.getElementById('container')
// 渲染函数
let vnode
function render(data) {
const newVnode = h('table', {}, data.map(item => {
const tds = []
for (let i in item) {
if (item.hasOwnProperty(i)) {
tds.push(h('td', {}, item[i] + ''))
}
}
return h('tr', {}, tds)
}))
if (vnode) {
// re-render
patch(vnode, newVnode)
} else {
// 初次渲染
patch(container, newVnode)
}
// 存储当前的 vnode 结果
vnode = newVnode
}
// 初次渲染
render(data)
const btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', () => {
data[1].age = 30
data[2].address = '深圳'
// re-render
render(data)
})
</script>
</body>
</html>
vdom总结
用JS模拟DOM结构(vnode) 新旧vnode对比,得出最小的更新范围,最后更新DOM 数据驱动视图的模式下,有效控制DOM操作
虚拟DOM-diff算法概述
diff算法是vdom中最核心、最关键的部分
diff算法能在日常使用vue React中体现出来(如key)
diff算法是前端热门话题,面试“宠儿”
diff即对比,是一个广泛的感念,如linux diff命令、git diff等
两个js对象也可以做diff,如github.com/cujojs/jiff
两棵树做diff,如这里的vdom diff
树diff的时间复杂度O(n^3)
第一,遍历tree1;第二,遍历tree2 第三,排序 1000个节点,要计算1亿次,算法不可用
优化时间复杂度到O(n)
只比较同一层级,不跨级比较
tag不相同,则直接删掉重建,不再深度比较
tag和key,两者都相同,则认为是相同节点,不再深度比较
深入diff算法源码
生成vnode
h函数
patch函数
执行pre hook 第一个参数不是vnode-创建一个空的vnode(emptyNodeAt),关联到这个DOM元素 判断vnode是否相同(sameVnode)-key和sel都相等 相同执行patchVnode 不相同,直接删掉重建
patchVnode函数(vnode对比)
-
执行prepatch hook(生命周期的钩子)
-
设置vnode.elem
-
vnode.text===undefined(vnode.children一般有值)
新旧都有children,updateChildren; 新children有,旧children无(旧text有),清空text,添加children(addVnodes); 旧children有,新children无,移除children(removeVnodes); 旧text有,清空 -
vnode.text!==undefined(vnode.children一般无值)
新旧text不一样,移除旧children,设置新text
updateChildren函数
- 开始和开始对比 patchVnode() 累加累减
- 结束和结束对比 patchVnode() 累加累减
- 开始和结束对比 patchVnode() 累加累减
- 结束和开始对比 patchVnode() 累加累减
- 以上四个都未命中 拿新节点key,能否对应上oldCh中的某个节点的key 没对应上,New element 对应上,拿到对应上key的节点,判断sel是否相等,不相等New element,相等patchVnode()
不使用key全部删掉然后插入,使用key直接移动过来,不用做销毁然后重新渲染的过程
虚拟DOM-考点总结和复习
diff算法总结
patchVnode addVnodes removeVnodes updateChildren(key的重要性)
vdom和diff-总结
细节不重要,updateChildren的过程也不重要,不要深究 vdom核心概念很重要:h、vnode、patch、diff、key等 vdom存在的价值更加重要:数据驱动视图,控制DOM操作
模板编译前置知识点-with语法
模板编译
模板是vue开发中最常用的部分,即与使用相关联的原理 它不是html,有指令、插值、JS表达式,到底是什么 面试不会直接问,但会通过“组件渲染和更新过程”考察 前置知识:JS的with语法 vue template complier将模板编译为render函数 执行render函数生成vnode
with语法
改变{}内自由变量的查找规则,当做obj属性来查找 如果找不到匹配的obj属性,就会报错 with要慎用,它打破了作用域规则,易读性变差
const obj = {a:100,b:200}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) //undefined
//使用with,能改变{}内自由变量的查找方式
//使用{}内自由变量,当做obj的属性来查找
with(obj) {
console.log(a)
console.log(b)
console.log(c) //会报错!!!
}
vue模板被编译成什么?
模板不是html,有指令、插值JS表达式,能实现判断、循环 html是标签语言,只有JS才能实现判断、循环(图灵完备的:能实现顺序执行、判断、循环) 因此,模板一定是转换为某种JS代码,即编译模板
const compiler = require('vue-template-compiler')
// 插值
// const template = `<p>{{message}}</p>`
// with(this){return _c('p',[_v(_s(message))])}
// with(this){return createElement('p',[createTextVNode(toString(message))])}
//this->new Vue({....})
// h -> vnode
// createElement -> vnode
// // 表达式
// const template = `<p>{{flag ? message : 'no message found'}}</p>`
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
// // 属性和动态属性
// const template = `
// <div id="div1" class="container">
// <img :src="imgUrl"/>
// </div>
// `
// with(this){return _c('div',
// {staticClass:"container",attrs:{"id":"div1"}},
// [
// _c('img',{attrs:{"src":imgUrl}})])}
// // 条件
// const template = `
// <div>
// <p v-if="flag === 'a'">A</p>
// <p v-else>B</p>
// </div>
// `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}
// 循环
// const template = `
// <ul>
// <li v-for="item in list" :key="item.id">{{item.title}}</li>
// </ul>
// `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
// 事件
// const template = `
// <button @click="clickHandler">submit</button>
// `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
// render 函数
// 返回 vnode
// patch
// 编译
const res = compiler.compile(template)
console.log(res.render)
// ---------------分割线--------------
// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
// target._o = markOnce;
// target._n = toNumber;
// target._s = toString;
// target._l = renderList;
// target._t = renderSlot;
// target._q = looseEqual;
// target._i = looseIndexOf;
// target._m = renderStatic;
// target._f = resolveFilter;
// target._k = checkKeyCodes;
// target._b = bindObjectProps;
// target._v = createTextVNode;
// target._e = createEmptyVNode;
// target._u = resolveScopedSlots;
// target._g = bindObjectListeners;
// target._d = bindDynamicKeys;
// target._p = prependModifier;
// }
编译模板
模板编译为render函数,执行render函数返回vnode 基于vnode再执行patch和diff(后面会讲) 使用webpack vue-loader,会在开发环境下编译模板(重要)
vue组件中使用render代替template
讲完模板编译,再讲这个render,就比较好理解了 在有些复杂情况中,不能用template,可以考虑用render React一直都用render(没有模板),和这里一样
总结
with语法 模板到render函数,再到vnode,再到渲染和更新 vue组件可以用render代替template
回顾和复习已学的知识点
总结组件 渲染/更新过程
一个组件渲染到页面,修改data触发更新(数据驱动视图) 其背后原理是什么,需要掌握哪些要点 考察对流程了解的全面程度
回顾学过的知识
响应式:监听data属性getter setter(包括数组) 模板编译:模板到render函数,再到vnode vdom:patch(elem,vnode)和patch(vnode,newVnode)
组件 渲染/更新过程
初次渲染过程 更新过程 异步渲染
vue组件是如何渲染和更新的
初次渲染过程
解析模板为render函数(或在开发环境已完成,vue-loader) 触发响应式,监听data属性getter setter 执行render函数,生成vnode,patch(elem,vnode)
执行render函数会触发getter
<p>{{message}}</p>
<script>
export default {
data(){
return {
message:'hello',//会触发get
city:'北京'//不会触发get,因为模板没用到,即和视图没关系
}
}
}
</script>
更新过程
修改data,触发setter(此前在getter中已被监听) 重新执行render函数,生成newVnode patch(vnode,newVnode)
完成流程图
异步渲染
回顾$nextTick
汇总data的修改,一次性更新视图
减少DOM操作次数,提高性能
总结1
渲染和响应式的关系 渲染和模板编译的关系 渲染和vdom的关系
总结2
初次渲染过程 更新过程 异步渲染
如何用JS实现hash路由
前端路由原理
稍微复杂一点的SPA,都需要路由
vue-router也是vue全家桶的标配之一
属于“和日常使用相关联的原理”,面试常考
回顾vue-router的路由模式
hash
H5 history
hash的特点
hash变化会触发网页跳转,即浏览器的前进、后退 hash变化不会刷新页面,SPA必须的特点 hash永远不会提交到server端(前端自生自灭)
<!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>hash test</title>
</head>
<body>
<p>hash test</p>
<button id="btn1">修改 hash</button>
<script>
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash:', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash:', location.hash)
})
// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
location.href = '#/user'
})
</script>
</body>
</html>
如何用JS实现H5 history路由
H5 history
用url规范的路由,但跳转不刷新页面 history.pushState window.onpopstate
正常页面浏览
github.com/xxx 刷新页面 github.com/xxx/yyy 刷新页面 github.com/xxx/yyy/zzz 刷新页面
改造成H5 history模式
github.com/xxx 刷新页面 github.com/xxx/yyy 前端跳转,不刷新页面 github.com/xxx/yyy/zzz 前端跳转,不刷新页面
<!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>history API test</title>
</head>
<body>
<p>history API test</p>
<button id="btn1">修改 url</button>
<script>
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
}
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
</script>
</body>
</html>
总结
hash--window.onhashchange H5 history--history.pushState和window.onpopstate H5 history需要后端支持
两者选择
to B的系统推荐用hash,简单易用,对url规范不敏感 to C的系统,可以考虑选择H5 history,但需要服务端支持 能选择简单的,就别用复杂的,要考虑成本和收益
vue原理-考点总结和复习
组件化
组件化的历史 数据驱动视图 MVVM
响应式
Object.defineProperty 监听对象(深度),监听数组 Object.defineProperty的缺点(Vue3用Proxy,后面会讲)
vdom和diff
应用背景 vnode结构 snabbdom使用:vnode h patch
模板编译
with语法 模板编译为render函数 执行render函数生成vnode
渲染过程
初次渲染过程 更新过程 异步渲染
前端路由
hash H5 history 两者对比