vue原理(大厂必考)
面试为何会考原理?
面试中如何考察?以何种方式?
vue原理包括哪些?
面试为何考察原理,又用不到?
-
知其然知其所以然--各行业通用的道理
-
了解原理,才能应用的更好(竞争激烈,择优录取)
比如:一个API用的熟但不知道原理,一个不熟但知道原理,会优先录用后者
-
大厂造轮子(有钱有资源,业务定制,技术KPI)
大厂有时需要造轮子,只会用API肯定不行
面试如何考察Vue原理?
1、我们讲的是面试时的考察重点,而不是考察细节。掌握好2 8原则。
因为面试是个非常现实的问题,不是为了考你而考你,而是为了筛选和区分候选者。筛选也有成本,因此会用20%时间了解你80%重要的东西。
2、和使用相关联的原理,例如vdom、模版渲染
3、会考察整体流程是否全面?
是否闭环,很多工作时需要你闭环的。
4、热门技术是否有深度?
是否对热门技术有关注。广度差不多,优先考虑深度的候选人。
组件化
响应式
vdom和diff
模版编译
渲染过程
前端路由
组件化基础
很久以前就有组件化
数据驱动视图(vue:MVVM,react:setState)
对比Vue React的组件化
数据驱动视图
Mvvm:数据驱动视图。通过修改视图就能改变数据,反过来也可以。像现在很多管理系统,如果是操作dom的方式改变视图,复杂程度是没办法想象的,有了mvvm,就可以直接修改数据就能改变视图。
官网图:
具体到代码中是怎么样的?
vue响应式(重点中的重点)
- 也就是组件data的数据一旦变化,立刻触发视图的更新
- 是实现数据驱动视图的第一步
- 会是考察vue原理的第一题
-
核心API-Object.defineProperty
-
如何实现响应式,代码演示
-
Object.defineProperty的一些缺点()
但也不意味着proxy就一定好,不能100%使用,
-
proxy存在兼容性问题,且无法polifill
Object.defineProperty基本用法
Object.defineProperty实现响应式
监听对象,监听数组
复杂对象,深度监听
几个缺点
代码演示
安装并启动服务:
observe-demo文件夹
observe.js文件展示
// 触发更新视图
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()
// ...
// }
// 所以需要重新定义个原型
// data.nums.push(4) 走的是这里
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) // 21 说明defineReactive的参数value一直存在内存中
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所以有 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
所以,我们可以提炼出
Object.defineProperty()缺点:
- 深度监听,需要一次性递归到底,一次性计算量大。
- 无法监听新增属性/删除属性(Vue.set,Vue.delete)
- 无法原生监听数组,需要特殊处理
那如何完善?
完善方式:
// 重新定义数组原型
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)
}
})
总结:
虚拟DOM(Virtual DOM)和diff
这么多dom,怎么知道操作正确
vdom最早是react提出来的,vue2引入了
解决方案-vdom(Virtual DOM)
请用js模拟以下dom
答题时,起码要有标签,属性,事件。
以上只是个基础,不同框架规范不一样。
模拟之后怎么去用?
通过snabbdom这个工具学习vdom(面试官:用过虚拟dom么)
snobby:[ˈsnɒbi],adj. 势利的;
snabbdom部分源码展示:
可以看出,vnode和我们上图的结构挺像的。用代码演示下用法。
代码演示
目录snabbdom-demo文件夹
patch一个用法是,直接渲染到dom当中,一个用法是更新dom。
更新前:
更新后:
只有2,3元素闪动了,说明只有2,3更新。
当它特别复杂的时候,我们就很难对比出哪些需要更新哪些不需要更新。
Vdom核心:只有改变的才更新。
用jq呢?
不使用snabbdom代码:
<!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>
那能不能在jq里实现,只更新那两条数据?可以自己写,写到最后发现自己造了个跟snabbdom差不多的轮子/库。但是不现实,复杂之后你是很难宅出来的。
用了snabbdom这个库:
<!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的价值,我们不用关心哪些变,哪些没变,往vdom上面一扔就好,自动会跟我们对比出来。
关注整个流程的全面度,不要陷入细节,较真,不然精力是不够的。
snabbdom的重点总结
- h函数
- vnode数据结构
- patch函数
vdom总结
-
用js模拟dom结构(vnode)
-
新旧vdnode对比,得出最小的更新范围,最后更新dom
不管你需求多复杂,多少人操作,都根据这个机制,得出最小范围,然后更新,这样数据量再大,性能也能不错。
-
数据驱动视图的模式下,有效控制dom操作
Vue react 作为最流行的俩框架,有成千上万人操作dom,你无法控制每个人的操作,怎么数据驱动视图,你也没办法控制,那怎么去满足所有的需求呢?那就是使用vdom。
diff(对比)算法
新旧vdnode对比,得出最小的更新范围,这个算法就是diff算法。
面试题:为何要在v-for中要有key?
学完就有答案了。
diff算法细节知道了,应对大厂框架的问题,在深度上是够了的,到顶了。
diff算法是什么
-
diff(对比)算法没有特殊描述,常指vdom的diff算法。但它不是vdom独创的,是一个广泛的概念,如linux diff命令、git diff等。
所以你要是跟后端说diff算法,他的理解可能和你不一样。
-
两个js对象也可以做diff, 如:github.com/cujojs/jiff
-
两颗树做diff,如这里的vdom diff。
树diff的时间复杂度O(n^3)
- 第一,遍历tree1;第二,遍历tree2;
- 第三,排序
- 1000个节点,要计算1亿次,算法不可用。
结论,不可用。
除此,O(n^2)也不常用,比如冒泡,数据一大还是不行的。
这就设计之初,最大的难题。
最终大神们优化后,时间复杂度只有O(n)
diff算法概述:图示讲解
Snabbdom源码解读
会过一下,千万不要较真,面试也不会考这些很细的地方。
不要认为自己看不懂,跟着来就好。
源码克隆下来。目录snabbdom-source文件夹。
重点看h函数,patch,vnode函数
h函数:s r c >h.ts
import { vnode, VNode, VNodeData } from './vnode';
export type VNodes = VNode[];
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>
import * as is from './is';
function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = 'http://www.w3.org/2000/svg';
if (sel !== 'foreignObject' && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
let childData = children[i].data;
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
}
}
}
}
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode;
export function h (sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}, children: any, text: any, i: number;
if (c !== undefined) {
if (b !== null) { data = b; }
if (is.array(c)) {
children = c;
} else if (is.primitive(c)) {
text = c;
} else if (c && c.sel) {
children = [c];
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b;
} else if (is.primitive(b)) {
text = b;
} else if (b && b.sel) {
children = [b];
} else { data = b; }
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
addNS(data, children, sel);
}
// 返回 vnode
return vnode(sel, data, children, text, undefined);
};
export default h;
vnode函数:
vnode.ts
import { Hooks } from './hooks';
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'
export type Key = string | number;
export interface VNode {
sel: string | undefined;
data: VNodeData | undefined;
children: Array<VNode | string> | undefined;
elm: Node | undefined;
text: string | undefined;
key: Key | undefined;
}
export interface VNodeData {
props?: Props;
attrs?: Attrs;
class?: Classes;
style?: VNodeStyle;
dataset?: Dataset;
on?: On;
hero?: Hero;
attachData?: AttachData;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: any[]; // for thunks
[key: string]: any; // for any other 3rd party module
}
export function vnode (sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
let key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
export default vnode;
重点提炼:
- 返回值是一个对象
- children, text是不能共存的,子元素要么是个文本,要么是个dom节点。
- elm是渲染到哪个元素上去的
init函数
看看怎么来的:init返回出来的。
snabbdom.ts源码中找init。
init()中的patch函数
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
function emptyNodeAt (elm: Element) {
const id = elm.id ? '#' + elm.id : '';
const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
// 注意:最后返回值中传入的是elm,就是说把空的vnode跟elm绑定。
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}
xxx省略xxx
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
// 执行 pre hook
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 第一个参数不是 vnode
if (!isVnode(oldVnode)) {
// 创建一个空的 vnode ,关联到这个 DOM 元素
// 一个vnode必须要有一个dom元素做关联,不然不知道更新到哪里去
oldVnode = emptyNodeAt(oldVnode);
}
// 相同的 vnode(key 和 sel 都相等)
if (sameVnode(oldVnode, vnode)) {
// vnode 对比
patchVnode(oldVnode, vnode, insertedVnodeQueue);
// 不同的 vnode ,直接删掉重建
} else {
elm = oldVnode.elm!;
parent = api.parentNode(elm);
// 重建
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
}
cbs:callback
源码中的hook是什么意思
调用的就是这个生命周期。
如果都没有传key呢?
第一个参数是sel,都相同。
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
// key 和 sel 都相等
// 如果都没有传key呢
// 如果都没传,结果是true:undefined === undefined // true
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
如果都没传,结果是true。
当然,这是单独直接写出来的,可以不传key;如果在循环体里面,就必须要传key。
第一次patch ,因为传入的不是一个vnode,就会先创建一个空的 vnode ,关联到这个 DOM 元素,然后可能会命中重建。
其他函数见源码解读注释。
updateChildren()函数
最复杂的一个
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0, newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
// 当旧开始小于等于旧结束,且,新开始小于新结束时
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 做为空处理
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
// 开始和开始对比 ,判断是否是相同节点(key 和 sel 都相等 就说明是相同的元素)
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
// 结束和结束对比
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
// 开始和结束对比
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
// 结束和开始对比
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
// 以上四个都未命中(也就是没有旧开始等于新开始,...四种特殊情况)
// 就会看新节点的key值,是否跟旧的某个节点的key相等
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 没对应上,比如上图中的e,和旧的节点没一个对应上,
// 就会重建或插入节点
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
newStartVnode = newCh[++newStartIdx];
// 对应上了 但也不能说明是相同节点 还要咋样?比对sel是否相等
} else {
// 对应上 key 的节点
elmToMove = oldCh[idxInOld];
// sel 是否相等(参考sameVnode 的条件)
if (elmToMove.sel !== newStartVnode.sel) {
// 不相等就会重建或插入节点
// New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
// sel 相等,key 相等
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
while干的活儿图解
开始/结束,和,开始结束 进行排列组合对比 (2*2=4种情况),通过sameVnode()判断是否是相同节点(key 和 sel 都相等 就说明是相同的元素)。
新旧节点如何对比?用sameVnode()函数。
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
// key 和 sel 都相等 就说明是相同的元素
// undefined === undefined // true
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
key在源码中的作用
有了key,会导致updateChildren算法更高效
为什么高效?
因为:在以上四个都未命中(也就是没有旧开始等于新开始,...四种特殊情况) 的情况下,就会看新节点的key值,是否跟旧的某个节点的key相等;不想等,就会新建节点;相等,再看sel是否相等;不相等,就会新建节点;相等,就patchVnode。
总结不使用keys和使用keys区别
不使用keys,老节点就会全部删掉,然后插入;使用了keys,就可以知道哪个是相同的,就可以直接移动到新的节点里去,避免旧的节点全部销毁,然后重建新的节点。
是不是使用了keys就万事大吉了?
也不是,假如你使用的是随机数,,每次不一样,也白搭;假如使用的是index值,你之前的节点有了顺序变化,就也会出现问题;
diff算法总结
patchVnode
addVnodes 和 removeVnodes
updateChildren(key的重要性)
vdom和diff算法-总结
细节不重要,updateChildren的过程也不重要,不要深究
vdom核心概念很重要:h\vnode\patch\diff\key等。
vdom存在的价值更加重要:数据驱动视图 ,控制dom操作。
模版编译
老师用的是剧情推动式的讲解方式。
- 模版是vue开发中常用部分,即与使用相关联的原理。
- 它不是html,有指令、插值、JS表达式,到底是什么?
- 面试不会直接问,一般会通过问“组件渲染和更新过程”来考察对vue整个流程的全面性。
可能相关面试题:浏览器如何渲染页面?
前置知识:
- js的with语法
- Vue template complier将模版编译为render函数
- 执行render函数生成vnode
js的with语法
要用,移动要和组内,或者高工,打招呼。
编译模版
1、模版不是html,有指令、插值、js表达式,能实现判断、循环
html只是个标签语言,配合css能实现显示隐藏,但实现不了判断、循环,所以模版编译一定是某种js。
2、html、css都不是图灵完备语言,只有js才能是。
什么是图灵完备的语言?就是能实现数据循环,判断,执行逻辑这三种的。
3、因此,模版一定是转换为某种js代码,即编译模版
编译模版-代码演示
安装
安装完之后
新建index.js文件
插值表达式编译成啥?
// 导入
const compiler = require('vue-template-compiler')
// 插值
const template = `<p>{{message}}</p>`
// 编译
const res = compiler.compile(template)
console.log(res.render)
打印结果:
this是什么?
const vm = new Vue({...}) // this是vue实例
下划线(_xxx)是什么?
指this,会从this里查找,也就是从vue的实例里找。
其中
这个挺像h函数
为什么没{},因为p标签里没有元素,只有一个字符串。
“_ c”是什么?
扒vue的没压缩的源码,找到“_ c”、“_ v”等,是什么?
// 从 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;
}
“_ c”不在此处,在别处,其意思是:“createElement”,比叫h函数,更具语义。
现在替换下打印的结果:
// with(this){return createElement('p',[createTextVNode(toString(message))])}
对比下之前打印的:
之前:执行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'))])}
还是js代码,没什么神奇魔术之类的,执行这个js,还是返回vnode。
属性和动态属性编译成?
// // 属性和动态属性
const template = `
<div id="div1" class="container">
<img :src="imgUrl"/>
</div>
`
条件编译成?
// 条件
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")])])}
// target._v = createTextVNode;
// v-if,v-else变成了三元表达式
循环编译成?
// 循环
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)}
// target._l = renderList; 这个实现也不难,返回若干个li标签
v-model原理?
面试题:请说一下v-model实现双向绑定的原理?
// 面试题:请说一下v-model实现双向绑定的原理
// 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}}})}
看 input 事件:
根据模版去渲染input的时候,就已经给他挂了个input事件,所以说它每次去input的时候,都能去把变量,进行更新,这样一更新,就能触发我们重新渲染,就能把新的name放到value上。
input渲染的时候,有几个要点:
- 1,value赋值的是个变量,变量的值取自this,也就是vue实例。
- 2,给input绑定了个on监听事件,将监听到的value值,赋值给实例的name (name=$event.target.value)。
总结:
以上函数都叫render 函数。
返回的都是 vnode 有了这个,就可以去做渲染,怎么渲染,之前讲过。至此,就可以了。
就不要去深挖'vue-template-compiler'包又做了什么了,违反了之前讲过的“和使用相关”的原则。
使用webpack的vue-loader优化
如果用webpack或者vue-cli的话,上面这部分工作,都是在开发环境下做的,所以产出的代码里就没有模版,全部变成了render函数的形式。
什么情况下不是开发环境下编译?
比如,你就是新建了个html,引入了vue.js,写个简单例子,那它就是在浏览器运行时编译的,而编译模版开销还是比较大的,导致这样比较慢。
所以,做项目时,一定要集成webpack的vue-loader,这样就可以在开发环境下去做编译模版。
vue组件中使用render代替template
要是没有学模版编译,就不知道以上时干嘛,或变一下也不知道。
总结
总结组件的渲染/更新过程
回顾vue三大核心知识点
1、响应式:监听data 属性getter setter(包括数组)
之前写过一个observe.js,在observe-demo文件夹下
核心代码
// 重新定义属性,监听起来
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()
}
}
})
}
核心 API:Object.defineProperty
深度监听:observer(value)
// 监听对象属性
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 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)
}
})
2、模板编译:模板到render函数,再到vnode
见模版编译
3、vnode:patch(elem,vnode)和patch(vnode,newVnode)
组件渲染/更新过程
初次渲染过程
即:city修改的时候,不会被监听到,即触发不到setter。
更新过程
完成流程图
面试的时候能把这个图,边画边讲出来,就89十分了。
异步渲染(这个概念非常重要)
回顾$nextTick
如果不加$nextTick,每次获取的都是错误的。
因为vue的组件是异步渲染的, $nextTick待dom渲染完再回调。
不太理解:老师说,虽然我们修改了三次,但是,页面渲染的时候,会将data的修改做整合,多次data的修改,会整合起来渲染一次。所以说,虽然我门修改了三次,但是nextTick只调用了一次。
所以vue异步渲染的作用:
1、汇总data的修改,一次性更新视图。
2、减少dom操作次数,提高性能。
如果说修改一次数据,就渲染一次,浏览器是受不了的。
不光是vue,react也是异步渲染的。
总结1
- 渲染和响应式的关系
- 渲染和模版编译的关系
- 渲染和vdom的关系
vue最核心的三个概念,串起来,就是模版的渲染和更新过程。
也就是那张图,建议把图手动画一遍。
总结2
初次渲染过程
更新过程
异步渲染(一定要知道)
前端路由原理
通用的,不局限于vue的,react也是。
- 稍微复杂点的spa,都需要路由(前端的路由,如果是后端的路由,就不叫spa了,就叫多页应用)。
- Vue-router也是vue全家桶的标配之一
- 属于“和日常使用相关联的原理”,面试常考
回顾vue-router的路由模式
- 哈希(hash)
- H5 history
mode=history,同时也需要后端的支持。
网页url组成部分
hash的特点
-
hash的变化会触发网页跳转,即浏览器的前进后退
-
hash的变化不会刷新页面,spa必需的特点。
像app样,前进后退了
-
永远不会提交到server端(前端自生自灭,后端不会参与)
代码演示(看看就好)
进入router-demo文件夹,打开终端,使用http-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>
更改hash 的变化
有很多种,包括:
- JS 修改 url
- 手动修改 url 的 hash,不会触发刷新,会触发前进后退。要是修改的其他部分,就会触发刷新。
- 浏览器前进、后退,也可能导致hash的变化。
点击之前
点击之后
只要知道监听、触发哈希的变化,就好,至于怎么触发的哈希变化,就不要去深究了,违背了“常使用”的原则。
哈希总结
1,监听哈希:window.onhashchange
window.onhashchange = (event) => {
console.log('old url', event.oldURL) // 完整的url
console.log('new url', event.newURL)
console.log('hash:', location.hash) // 哈希
}
2,获取哈希:location.hash
3,改变哈希:三种,手动修改地址栏哈希,浏览器前进后退,js
j s:触发监听,在监听回调里 使用location.href 。
eg:location.href = '#/user'
H5 history
哈希是跟后端没交互的,属于前端路由。
H5 history:
1、是用使用url规范的路由 。
2、跳转时也不刷新页面(当然啊,不然我们spa应用还能用它! )。
3、使用history.pushState和 window.onpopstate实现。
正常页面:
地址后面输入啥,都会刷新页面
改造成H5 history模式
表现跟哈希是一样的,但是是符合地址规范的。
代码演示
<!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')
// pushState(对象,空就好,路由的path)
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>
前进或后退的时候,path都会打印出来。
两个核心API:
1、history.pushState(对象,空就好,路由的path)
【注意】用 pushState 方式,浏览器不会刷新页面
2、window.onpopstate:监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
// event.state就是我们之前定义的state对象,可以用来传值
}
后端要做的事
不管什么地址/路由,统统给我返回到主页面地址,完了,全部通过前端自己的方式去跳转页面,后端不要管。
例如:
做了兼容处理,即每次不管什么样的路由,都给我返回主页面,然后,通过
history.pushState(对象,空就好,路由的path)的方式,去做页面的切换,去访问其他路由。
总结
两者如何选择
当你的项目,需要做SEO搜索优化的时候,可以去选择复杂的history模式。
总的来说:
能简单实现就简单点,别用复杂的,要考虑成本。
vue原理-总结
组件化:
vue实现的方式是mvvm,react实现的方式是setState(没记错的话)
响应式:
Object.defineProperty缺点有三个。
vdom和diff
首先要用js模拟dom的结构,才能实现虚拟dom(vdom)和diff算法的过程。
模版编译
组件渲染/更新过程
那个图要能画出来!!!
前端路由原理
下一篇,开大餐!真题演练!
完结,撒花撒花!
累死宝宝了,3万4千多字符~累趴