整理一些重点:
- 组件化和MVVM
- 响应式原理
- vdom和diff算法
- 模板编译
- 组件渲染过程
- 前端路由
一、组件化基础
在组件化基础上,实现了数据驱动视图(MVVM)
传统组件只是静态渲染,更新还要依赖于操作DOM
而MVVM——不用操作DOM 直接修改数据就能够改变视图
结合代码分析view如何修改model
整理一下自己的理解:
组件化是一个较早之前就有的理念,但传统的组件化,只是做静态渲染,更新依赖于直接操作DOM
而Vue在组件化的基础上,加入了创新——数据驱动视图MVVM
通过MVVM,我们不再需要直接操控DOM元素,而是来操作model(也就是data),通过指令来更新View视图层,相应的,视图层的DOM也可以通过聆听来改变model数据
二、Vue响应式
重点!!!
组件data的数据一旦变化,立刻触发视图的更新,这也是时间数据驱动试图的第一步
核心API-Object.defineProperty
但Vue3.0启用了Proxy来启动响应式了(因为Object.defineProperty存在一些缺点)
基本使用:
Object.defineProperty() 方***直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
其语法:
Object.defineProperty(obj,prop,descriptor)obj——要定义属性的对象 prop——要定义或修改的属性的名称或 Symbol descriptor——要定义或修改的属性描述符
const data={}
const name='zhangsan'
Object.defineProperty(data, "name", {
get: function() {
console.log('get!');
return name
},
set: function(newVal) {
console.log('set!');
name=newVal;
}
});
//通过以上代码 就可以get 和 set data中的name属性了 基于Object.defineProperty这个函数,写最简单的监听函数:
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 核心监听函数
function defineReactive(target, key, value) {
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 如果传进来的不是对象或数组 就直接返回
return target
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
}
监听数据
observer(data)
// 测试
data.name = 'lisi'//相当于调用了set函数 触发了更新视图函数updateView()
data.age = 21
深度监听:
但上述函数无法深度监听 我们只能监听到key这一层
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// value很可能是个对象 之前普通监听没有value的判断 所以调用observer 可以进行判断
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 设置newVal的时候可能设置了一个对象做新值 也需要深度监听
observer(newValue)
// 设置新值
// 注意,value一直在闭包中,此处设置完之后,再get时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
}
}
// 监听数据
observer(data)
// 测试
data.name = 'lisi'
data.age = 21
data.info.address = '上海' // 深度监听
说白了,我们做深度监听的时候,一定要保证,每个key 都被Object.defineProperty挂上了value 这样key值发生set时才能被监听到但有此时还有很多缺点:
- 深度监听递归到底,一次性计算量大,可能会导致卡死
- 新增属性和删除属性时无法监听(Vue.set Vue.delete)
- 无法原生监听数组 需要特殊梳理
监听数组:
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建一个空新对象,原型指向oldArrayProperty,对arrProto扩展新的方法不会影响原型
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: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
data.nums.push(4) // 监听数组 代码很长,我把最核心的改动部分再单独拿出来一下:// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建一个空新对象,原型指向oldArrayProperty,对arrProto扩展新的方法不会影响原型
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)
VDOM是实现VUE的基础,diff就是VDOM的最核心、最关键的部分。
VDOM就是用JS来模拟DOM结构,因为JS执行速度很快,这样可以计算出最小的变更,来操作DOM
用JS模拟DOM结构
传统JS结构:
用JS模拟DOM:主要就是 tag props children 三大元素
注:class-classname
以上代码通过JS实现了vnode 但这只是一个节点 还不是咱们需要的VDOM
VDOM可以进行新旧vnode的对比,得出最小的更新范围,最后更新DOM
学习VDOM可以先了解snabbdomhttps://github.com/snabbdom/snabbdom:h函数 vnode数据结构 patch函数
vue就是基础snabbdom实现的VDOM 它是一个简介强大的vdom库 非常简单
**diff算法
前文提到了,VDOM可以进行新旧vnode的对比,得出最小的更新范围,最后更新DOM
这个更新过程就是diff算法
diff算法概述
先来了解一下diff算法概述
原本树diff的时间复杂度非常高 达到了O(n^3)
因此,对diff进行了优化,时间复杂度优化到了O(n)
- 只比较同一层级
- 比较tag 若tag不同 就不比较了 直接删掉重建拉到
- tag和key都相同,则认为是相同节点,也不再做深度比较
要记住:diff的作用,就是以代价最小的方式来更新DOM ,所以会尽可能的复用DOM。
vue的VDOM是在snabbdom的基础上实现的,下面就是品味snabbdom的源码,领会这个过程:
h函数:
h函数就是vue中的createElement方法,这个函数作用就是创建虚拟dom,追踪dom变化的 它返回的就是Vnode
说白了 就是生成vnode的函数
接受三个参数:
- tag(标签名)、组件的选项对象、函数(必选);
- 一个对象,标签的属性对应的数据(可选);
- 子级虚拟节点,字符串形式或数组形式,子级虚拟节点也需要使用createElement构建。
h函数返回的也是一个函数
vnode函数返回的也是一个vnode结构的对象
patch函数:
patch就是实现虚拟DOM渲染的这个方法
通过patch可以进行新旧两个vnode的差异对比,但这只是手段,不是目的,它的最终目的还是渲染视图
patch函数有两种用法:
patch(container,vnode) patch(vnode,newVnode)
- 首次渲染,vnode把结构全部塞给container里面去
- 当数据改变时,第一个参数vnode是之前的结构,newVnode是改变后的结构,首先这两者先进行对比,找出不同的地方,只渲染不相同的地方,减少DOM的操作。
看一下源码,发现patch是由.init函数返回的:
去找这个snabbdom.init函数:
发现其返回结果就是一个patch函数:
这个patch函数 也就是我们要重点分析 接下来通过分析源码 领会这个patch函数:
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 元素
oldVnode = emptyNodeAt(oldVnode);
}
// 相同的 vnode(key 和 sel 都相等) 进行patchVnode对比
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);
}
}
那么如何对比呢? 首先进行sameVnode的判断 如果都相等 就进行patchVnode的对比
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
// key 和 sel 都相等
// undefined === undefined // true
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
} function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// 执行 prepatch hook 就相当于一个生命周期钩子
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
// 设置新的vnode.elem 因为此时新的vnode是断线风筝 不知道更新哪个element
const elm = vnode.elm = oldVnode.elm!;
// 旧 children
let oldCh = oldVnode.children as VNode[];
// 新 children
let ch = vnode.children as VNode[];
if (oldVnode === vnode) return;
// hook 相关 先不看
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
vnode.data.hook?.update?.(oldVnode, vnode);
}
// vnode.text === undefined (意味着一般情况下 vnode.children有值)
if (isUndef(vnode.text)) {
// 新旧都有children
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
// 新children有,旧children无 (旧text有)
} else if (isDef(ch)) {
// 清空 text
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 添加 children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
// 旧child有,新child无
} else if (isDef(oldCh)) {
// 移除 children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
// 旧 text 有 新text无
} else if (isDef(oldVnode.text)) {
//清空text
api.setTextContent(elm, '');
}
// else: vnode.text有值(也就是vnode.children无值)
//新旧text值不相等
} else if (oldVnode.text !== vnode.text) {
// 移除旧 children
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 设置新 text
api.setTextContent(elm, vnode.text!);
}
hook?.postpatch?.(oldVnode, vnode);
}
其中重点 就是新旧都是children时 进行updateChildren 指针向中间移动,当两个指针重合的时候,循环结束
那么如何对比呢?
// 开始和开始对比
} 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]; 如果sameVnode 就命中啦!! (这种对比方式不要较真 因为这是细节问题 可能不一样) 如果以上都没命中
// 以上四个都未命中
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 拿当前新节点的key 看它能否对应上oldCh中的某个节点的key
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 没对应上 就新建 插入
if (isUndef(idxInOld)) { //
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
newStartVnode = newCh[++newStartIdx];
// 对应上了
} else {
// 拿到oldCh中对应上 key 的节点
elmToMove = oldCh[idxInOld];
// 判断sel 是否相等(sameVnode 的条件)sel不相等 就新建
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];
}
} 如果使用index作为key的话,当children有顺序上的变化时,就不好判断了
一个小结:
| 判断是否为sameVnode | 就是看key和tag是否相同 相同的话 进行patchVnode 进一步比较 不同的话 直接替换成新的 没有比较必要 |
| patchVnode | 只有文本不一样 修改文本 旧的有子节点 新的没有子节点 删除子节点 removeVnodes 旧的没有子节点 新的有子节点 新增子节点 addVnodes 新的旧的都有子节点 且子节点不相同 进行updateChildren |
| updateChildren | 新旧两组子节点 首尾都置指针 比较过程参考上面复制的那篇文章 |
三、模板编译
模板是vue中最常用的部分,看起来很像html 但又不是html 浏览器无法识别 他有指令有插值有JS表达式 还能做循环和片段
html就是一个标签语言 只有JS才能够实现这些逻辑
因此,模板一定是转成了某些JS代码
先要学习JS的with语法
(并不常用,但是模板编译需要用到)
也就是改变{ }内自由变量的查找规则 当作obj的属性来查找 查不到匹配的obj属性就会报错 他打破了作用域的规则
在with的基础上 我们来学习 vue模板究竟被编译成了什么?
模板编译
with(this)就相当于h函数,返回了一个vnode
this就是vue实例
当只有一个插值时:
// 插值
const template = `<p>{{message}}</p>`
// 输出结果:with(this){return _c('p',[_v(_s(message))])}
//相当于 with(this){return createElement('p',[createTextVNode(toString(message))])}
当表达式时:/ 表达式
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}}})}
//我们在渲染时 已经绑定了一个input事件
// 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;
// } 以上那些生成的类似vnode函数,都被叫做render函数 会返回一个vnode 模板编译过程说白了就
- 将模板编译为render函数,执行render函数返回vnode
- 在生成vnode后,再执行patch和diff
- 实际项目中,使用webpack vue-loader 会在开发环境下编译模板
在vue组件中,可以使用render代替template

很显然 template更好
但React一直都用的render
四、渲染更新
最后一部分来了!!!!
分为三个部分
- 初次渲染过程
- 更新过程
- 异步渲染
初次渲染过程
- 解析模板为render函数(或在开发环境中已经完成 vue-loader)
- 触发响应式 监听data属性 getter setter
- 执行render函数 生成vnode 执行patch(elem,vnode)
第二步看起来没有用,但其实不然,在执行render函数 就会触发getter
更新过程
- 修改data 触发stter 但要看一下这个触发了setter的函数是否在之前的getter中已经监听 (说白了 就是看这个修改了的data模板有没有用到,和视图有没有关系,如果是像上图代码中的city 修改了也没事 它和视图无关啊!不用重新渲染)
- 重新执行render 生成newVnode
- patch(vnode,newVnode)
*异步渲染
非常非常重要!和$nextTick结合着看
异步渲染会汇总data的修改,一次性更新视图,从而减少DOM操作次数,提高性能
小结:
VUE高级原理最重要的就是响应式、模板编译、VDOM、渲染这四大块。他们是息息相关的
要牢牢记住:
- 渲染和响应式的关系
- 渲染和模板编译的关系
- 渲染和VDOM的关系
五、路由
路由原理
回顾vue-router的路由模式
hash
H5 history
首先分析一下网页url组成部分
hash方法
- hash变化会触发网页跳转 即浏览器的前进后退
- hash变化不会刷新页面 这也是SPA必须的特点
- hash永远不会提交到server端 在前端自生自灭
- JS 修改 url
- 手动修改 url 的 hash
- 浏览器前进、后退
代码演示:
通过JS设置hash
通过window.onhashchange这个API监听hash变化
<!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> H5 history
- 用url规范的路由,但是跳转时不刷新页面(与hasn不一样)
- 通过history.pushState window.onpopstate实现
- 需要server端的配合
正常页面浏览时:
H5 history
代码演示:
重点是切换路由的API
history.pushState(state, '', 'page1')
以及 监听路由变化的API
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
} 整体代码:<!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> 需要后端的配合 不管访问什么路由 都返回一个index.html的路由 再从这个index.html页面,通过history.pushState跳转到需要的路由
选择:
to B(后台)的系统推荐用hash 简单易用
to C的系统,可以考虑H5 history
还是hash更简单

京公网安备 11010502036488号