Vue深入浅出之原理篇
响应式
Vue2实现响应式--Object.defineProperty
缺点:
Object.defineProperty
不能监听数组,因此需要特殊处理(重新定义数组原型)- 实现深度监听是利用递归,需要一次去递归到底,计算量大,data层级不要太深
- 无法监听新增属性/删除属性(
Vue.set
、Vue.delete
解决)
下面是简化后的模拟vue2响应式原理代码:
// 触发视图更新
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)
}
})
// 重新定义属性监听
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心api
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
value = newValue
// 触发更新
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或者数组
return target
}
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 数据
const data = {
name: 'AirHua',
age: 20,
info: {
address: '成都'
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
data.name = 'huabyte'
data.info.address = '上海' // 需要深度监听
data.nums.push(40) // 监听数组失败,需要单独处理
Vue3实现响应式--Proxy
- 主要靠代理配置实现监听,通过get和set来获取响应式,只有用到才会去监听
- 对象、数组都可以监听
- 搭配
Reflect
使用
下面是简化后的模拟vue3响应式原理代码:
// 触发视图更新
function updateView() {
console.log('视图更新')
}
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则返回
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key)
}
const result = Reflect.get(target, key, receiver)
return reactive(result)
},
set(target, key, val, receiver) {
// 重复数据不处理
if (val === target[key]) {
return true
}
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('修改属性')
updateView()
} else {
console.log('新增属性')
updateView()
}
const result = Reflect.set(target, key, val, receiver)
// 是否设置成功
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('删除属性')
return result
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
// 数据
const data = {
name: 'AirHua',
age: 20,
info: {
city: '成都',
tags: {
life: {
days: 365,
year: 1
},
flag: [1, 3, 5]
}
}
}
// 开启监听
const proxyData = reactive(data)
// 测试
proxyData.info.city = 'huabyte11'
delete proxyData.info
vdom和diff算法(重点)
再聊vdom前,我们先来想想为什么会有vdom这个概念?在传统的网页里,我们操控网页都是直接操作DOM,而当业务需求大起来的时候,需要大量操作DOM,这无疑很消耗性能。
前端框架中,都不约而同地采用了同一个思路,前端框架会将组件先转换为虚拟 DOM 节点,即 Virtual DOM,之后再将虚拟 DOM 节点渲染成实际的 DOM 节点,Virtual DOM 也会被组织成树形结构,即 Virtual DOM 树。这样把DOM操作先通过js计算出最小变更,去操作DOM,这里面算法用到的就是diff算法。
vdom
vdom节点是一个规范化的数据结构,类似如下:
{
tag: 'div',
props: {
className: 'container',
id: 'div1'
},
children: [{
tag: 'p',
children: '测试文本'
},
{
tag: 'ul',
props: {
style: 'font-size: 20px'
},
children: [{
tag: 'li',
children: 'li-text'
}]
}
]
}
用这个数据结构,最终可以转化为DOM节点,等价于如下DOM元素:
<div id="div1" class="container">
<p>测试文本</p>
<ul style="font-size: 20px;">
<li>li-text</li>
</ul>
</div>
接下来我们说说怎么实现的vdom,vue2中基于Snabbdom实现,看看官网给的例子:
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]);
const container = document.getElementById("container");
const vnode = h("div#container.two.classes", {
on: {
click: someFn
}
}, [
h("span", {
style: {
fontWeight: "bold"
}
}, "This is bold"),
" and this is just normal text",
h("a", {
props: {
href: "/foo"
}
}, "I'll take you places!"),
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
const newVnode = h(
"div#container.two.classes", {
on: {
click: anotherEventHandler
}
},
[
h(
"span", {
style: {
fontWeight: "normal",
fontStyle: "italic"
}
},
"This is now italic type"
),
" and this is still just normal text",
h("a", {
props: {
href: "/bar"
}
}, "I'll take you places!"),
]
);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
-
h函数:渲染出vnode
-
patch:对比计算新旧vnode变化
其中
patch
的实现就是用了diff算法
diff算法概述
可以想象,两个vnode,需要对比的话
- 遍历vnode
- 遍历newVnode
- 排序
需要实现这样的算法的时间复杂度为O(n^3),算法不可用,因此需要把算法优化。
于是就有了这样最开始的实现:
- 只比较同一层级,不跨级比较
- tag不相同,则直接删掉重建,不再深度比较
- tag和key,两者都相同,则认为是相同节点,不再深度比较
这样就把时间复杂度降到了O(n)
那么具体是怎么做到的呢,我们还是以Snabbdom部分源码来看:
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 都相等)
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;
};
可以看到,最为关键的是当vnode中相同时,patchVnode
的执行,看看它的源码:
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// 执行 prepatch hook
const hook = vnode.data ? .hook;
hook ? .prepatch ? .(oldVnode, vnode);
// 设置 vnode.elem
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 有
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}
// else : vnode.text !== undefined (vnode.children 无值)
} 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);
}
接着可以看到当新旧vnode都有children时需要去执行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];
// 开始和开始对比
} 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];
// 以上四个都未命中
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 没对应上
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
newStartVnode = newCh[++newStartIdx];
// 对应上了
} 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);
}
}
}
说说updateChildren
里面是怎么实现的吧:
- 开始和开始比较
- 结束和结束比较
- 开始和结束比较
- 结束和开始比较
如果匹配成功,则循环向前或者向后比较,若四种情况都没有,则通过key来找oldCh中是否有匹配项,这里也说明key的重要性。
v-for的key为什么必要
- 也解释了为什么key需要唯一,当没有key则省略去了一个匹配规则
- key也最好不要设置为index,因为随机的当顺序换了,index也随之改变,导致也匹配不到
描述组件渲染过程
初次渲染过程
- 解析模板为render函数
- 触发响应式,监听Data,通过getter获取值,setter修改值
- 执行render函数,生成vnode,初次patch(elem, vnode)
更新过程
- 修改Data,触发setter
- 重新执行render函数,生成newVnode
- patch(vnode, newVnode)
完整流程图
对MVVM模型的理解
Model
:代表数据模型,数据和业务逻辑都在Model层中定义View
:代表UI视图,负责数据的展示ViewMdel
:负责监听Model中数据改变控制View视图的更新
用图表示就是这样:
用代码描述的话:
template
可以理解为View
层
<template>
<div>
<ul ref="ul">
<li v-for="(item, index) in list" :key="index">{{item}}</li>
</ul>
<button @click="addItem">添加</button>
</div>
</template>
data
可以理解为Model
层
data() {
return {
list: ['a', 'b', 'c'],
}
},
template
里面绑定的方法,和script
里面定义的方法就可以理解为ViewModel
methods: {
addItem() {
this.list.push(`${new Date()}`)
this.list.push(`${new Date()}`)
this.list.push(`${new Date()}`)
this.$nextTick(() => {
const ulElem = this.$refs.ul
console.log(ulElem.childNodes.length)
})
},
},
// ...