开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第13天,点击查看活动详情
diff算法,虚拟dom
虚拟节点与DOM Diff算法源码实现:→视频学习
为什么需要虚拟DOM?
虚拟dom的优势:对数据进行了封装 ,还有数据发生变化有劫持 ,然后优秀的diff算法找到哪里发生了变化,那么只需重新渲染局部的内容就可,性能大大提升↑↑↑
一句话理解:替换更新后的节点,减少频繁对dom的操作
什么是diff算法?
diff优化算法:1、只比较同一层级,不跨级对比;
2、标签名不同直接删除,不深度比较;
3、标签名相同,key也相同,就认为是相同节点,不深度比较
patch函数
snabbdom文档:一个虚拟DOM库
(下面的都不是完整的代码↓)
/**
* 数据变化 --> 虚拟dom(计算变更) ---> 操作真实dom ---> 更新视图
*
*/
// patch是通过init函数创建出来的
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); // 首次渲染执行一次,目的:把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); // 页面更新时候,patch会把新vnode替换旧vnode
init.ts文档
......
// patch函数
return function patch(
oldVnode: VNode | Element | DocumentFragment,
vnode: VNode
): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
/**
* 首次渲染,传入patch函数的第一个参数是个dom元素,
* 命中这个逻辑:通过emptyNodeAt方法创建一个空的vnode,并关联dom元素
*/
if (isElement(api, oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
} else if (isDocumentFragment(api, oldVnode)) {
oldVnode = emptyDocumentFragmentAt(oldVnode);
}
// 之后更新渲染调用
// 根据传进来的两个参数是否为同一vnode进行判断
if (sameVnode(oldVnode, vnode)) {
// key,tag相同,说明同一个vnode,直接patch做更新
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// key,tag不同,说明不同的vnode
elm = oldVnode.elm!;
parent = api.parentNode(elm) as Node;
// 创建新的dom元素
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 插入新的dom元素
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;
};
// emptyNodeAt函数
// sameVnode函数
// patchVnode函数
// createElm函数
// removeVnodes函数
patchVnode函数
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
// 执行hook
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
// 设置新vnode关联的dom元素
const elm = (vnode.elm = oldVnode.elm)!;
// 新老vnode一样,不操作
if (oldVnode === vnode) return;
// 与hook相关操作
if (
vnode.data !== undefined ||
(isDef(vnode.text) && vnode.text !== oldVnode.text)
) {
vnode.data ??= {};
oldVnode.data ??= {};
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
vnode.data?.hook?.update?.(oldVnode, vnode);
}
// 老children
const oldCh = oldVnode.children as VNode[];
// 新children
const ch = vnode.children as VNode[];
// 新vnode:无text有children
if (isUndef(vnode.text)) {
// 1.1 新vnode:有children,旧vnode--有children
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
//1.2 新vnode:有children,旧vnode 无children有text
} else if (isDef(ch)) {
// 清空text
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
// 添加children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
//1.3 新vnode 无children,旧vnode 有children
} else if (isDef(oldCh)) {
// 移除children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
//1.4 旧vnode 有text
} else if (isDef(oldVnode.text)) {
// 清空text
api.setTextContent(elm, "");
}
//2、 新vnode:有text无children
//2、 旧vnod text 不等于 新vnode text
} else if (oldVnode.text !== vnode.text) {
// 旧vnode 有children
if (isDef(oldCh)) {
// 移除旧vnode children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 设置新vnode text
api.setTextContent(elm, vnode.text!);
}
hook?.postpatch?.(oldVnode, vnode);
}
updateChildren函数
新老children的增删改(排序)
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
let oldStartIdx = 0;
let 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];
// 1、老开始和新开始对比
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
// 2、老结束和新结束对比
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
// 3、老开始和新结束对比
} 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];
// 4、老结束和新开始对比
} 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,在老children里找有没有某个节点是对应的这个key
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 没有在老children里面找到:创建新元素
if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
// 在老children找到了
} else {
// 找到这个元素
elmToMove = oldCh[idxInOld];
// 判断tag(标签)是否相等
if (elmToMove.sel !== newStartVnode.sel) { // tag不相等
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else { // tag相等
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (newStartIdx <= newEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
}
if (oldStartIdx <= oldEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
vue2响应式
let data = {
name: 'lily',
age: 22,
friend: {
friendName: 'grace'
},
colors: ['red', 'yellow', 'blue']
}
// 改写数组方法
const oldArrayProto = Array.prototype
// 以旧数组原型作为原型创建新数组对象
const newArrayProto = Object.create(oldArrayProto)
// 往newArrayProto空对象新添方法
['push', 'pop', 'shift', 'unshift'].forEach(element => {
newArrayProto[element] = function () {
console.log('改写数组方法')
oldArrayProto[element].call(this, ...arguments)
}
});
// 把data变成响应式数据
observer(data)
function observer(target) {
// 判断target的数据类型
if (typeof target !== 'object' || target === null) {
return target
}
if (Array.isArray(target)) {
target.__proto__ = newArrayProto
}
for (let key in target) {
defineReactive(target, key, target[key])
}
}
function defineReactive(target, key, value) {
// 对属性值深度监听
observer(value)
// 响应式原理
Object.defineProperty(target, key, {
get: function () {
return value
},
set: function (newValue) {
// 如果我们改变的值为object类型,再当改变该值也需要深度监听
observer(newValue)
if (newValue !== value) {
value = newValue
console.log('视图更新')
}
}
})
}
// console.log(data.name)
// data.age = 66
// console.log(data.age)
// 深度监听对象类型
// data.friend.friendName = 'gessi'
// data.name = {str:'ben'}
// data.name.str = 'jackson'
// 监听数组方法
data.colors[0] = 'orange'
data.colors.push('green')
// 不会监听到属性删除,新增
// delete data.age // Vue.delete
// data.test = 'something' // Vue.set
Object.defineProperty问题:
- 1、对数据深度监听才能数据改变时候视图更新,但一旦数据层级比较多比较复杂的话,一直深度监听到为普通值,那么页面一开始渲染会卡死; (这个问题在vue3的proxy可以解决:在使用到这个数据时候才对它进行深度监听的过程)
- 2、如果对象新增,删除属性也不会监听到,需要借助其他api
nextTick
nextTick应用:
例子:点击按钮往列表新添加数据,后台打印列表length,还是旧数组长度,但是页面已经是新数据内容?
(当我们在更新数据后立马去获取DOM中的内容时会发现获取的依然还是旧的内容)
因为在vue里面,dom渲染是异步的,当我们往数组添加了数据,但dom不是马上更新
那么,我们怎么同步拿到最新的数据并渲染到页面?
nextTick(vue实例上的一个实例方法)
this.nextTick(()=>{
// 回调函数里面能够拿到最新的数据信息
})
<template>
<div>
<h2>nextTick获取列表长度</h2>
<ul ref="list">
<li v-for="(item, index) in list" :key="index">
{{ item }}</li>
</ul>
<button @click="additem">click</button>
</div>
</template>
<script>
export default {
components: {},
props: {},
data() {
return {
list: ['列表1', '列表2', '列表3']
};
},
methods: {
additem() {
this.list.push(Date.now())
this.list.push(Date.now())
this.list.push(Date.now())
// const listref = this.$refs.list
// console.log(listref.chilNodes.length)
// vue中的DOM更新是批量处理的:上面执行了三次操作,但nextTick只执行一次
this.nextTick(() => {
const listref = this.$refs.list
console.log(listref.chilNodes.length)
})
}
}
};
</script>
什么时候触发nextTick?
nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的
mixin
抽离组件公共逻辑,本质是一个对象
// mixin
export default{
data(){
return {
xxxx
}
},
methods:{......},
mounted(){......},
.... // 内容跟vue组件一样
}
// 组件里引用
import mixin from 'xxxxxx'
mixins:[mixin]
注意:
1、组件和 mixin 的 data 和 事件重复时,就用 组件的 替换 mixin 的,没有重复就用 mixin的;
2、当生命周期钩子重复时,两者是并存的,先执行 mixin ,再执行组件的\
现在好像mixin比较少用了!!!
生命周期
Vue实例从创建到销毁的过程就是生命周期,既指从创建、初始化数据、编译模板、挂载DOM到渲染、更新渲染、销毁等一系列过程,他主要分为8个阶段,创建前后、载入前后、更新前后、销毁前、销毁后、以及一些特殊场景的生命周期。
| 生命周期 | 描述 |
|---|---|
| beforeCreate | 组件实例还没创建,通常用于插件开发中初始任务 |
| created | 组件初始化完毕,数据可用,常用于异步数据获取 |
| beforeMount | 未执行渲染,更新,dom还没创建 |
| mounted | 初始化结束,dom创建完毕,可获取访问数据和dom元素 |
| beforeUpdate | 更新前,可获取更新前的各种状态 |
| updated | 更新后,所有状态是最新的 |
| beforeDestroy | 销毁前,可用于一些定时器或订阅器的取消 |
| destroyed | 组件已销毁 |
created 和 mounted 区别?
created:在组件实例一旦创建完成时立刻调用,dom节点还没生成;
mounted:在页面dom节点渲染完后立刻执行;
相同点:都能拿到实例对象的属性和方法;但是放在mounted的请求,有可能导致页面闪动,所以一般数据请求放在页面加载前完成最好。