update courseware

This commit is contained in:
xie jie 2024-09-18 15:08:22 +08:00
parent faef9237f9
commit 8c7a28d153
112 changed files with 1752 additions and 0 deletions

View File

@ -0,0 +1,485 @@
# 渲染器核心功能
>面试题:说一说渲染器的核心功能是什么?
渲染器的核心功能,是根据拿到的 vnode进行节点的**挂载**与**更新**。
**挂载属性**
vnode
```js
const vnode = {
type: 'div',
// props 对应的就是节点的属性
props: {
id: 'foo'
},
children: [
type: 'p',
children: 'hello'
]
}
```
渲染器内部有一个 mountElement 方法:
```js
function mountElement(vnode, container){
// 根据节点类型创建对应的DOM节点
const el = document.createElement(vnode.type);
// 省略children的处理
// 对属性的处理
if(vnode.props){
for(const key in vnode.props){
el.setAttribute(key, vnode.props[key])
}
}
insert(el, container);
}
```
除了使用setAttribute方法来设置属性以外也可以使用DOM对象的方式
```js
if(vnode.props){
for(const key in vnode.props){
// el.setAttribute(key, vnode.props[key])
el[key] = vnode.props[key];
}
}
```
思考🤔:哪种设置方法好?两种设置方法有区别吗?应该使用哪种来设置?
**HTML Attributes**
Attributes 是元素的**初始**属性值,在 HTML 标签中定义,用于**描述元素的初始状态**。
- 在元素被解析的时候,只会初始化一次
- 只能是字符串值,而且这个值仅代表初始的状态,无法反应运行时的变化
```vue
<input type="text" id="username" value="John">
```
**DOM Properties**
Properties 是 JavaScript 对象上的属性,代表了 DOM 元素在 **内存中** 的实际状态。
- 反应的是 DOM 元素的当前状态
- 属性类型可以是字符串、数字、布尔值、对象之类的
很多 HTML attributes 在 DOM 对象上有与之相同的 DOM Properties例如
| HTML attributes | DOM properties |
| --------------- | -------------- |
| id="username" | el.id |
| type="text" | el.type |
| value="John" | el.value |
但是,两者并不总是相等的,例如:
| HTML attributes | DOM properties |
| --------------- | -------------- |
| class="foo" | el.className |
还有很多其他的情况:
- HTML attributes 有但是 DOM properties 没有的属性:例如 aria-* 之类的HTML Attributes
- DOM properties 有但是 HTML attributes 没有的属性:例如 el.textContent
- 一个 HTML attributes 关联多个 DOM properties 的情况:例如 value="xxx" 和 el.value 以及 el.defaultValue 都有关联
另外,在设置的时候,不是单纯的用某一种方式,而是两种方式结合使用。因为需要考虑很多特殊情况:
1. disabled
2. 只读属性
**1. disabled**
模板:我们想要渲染的按钮是非禁用状态
```vue
<button :disabled="false">Button</button>
```
vnode:
```js
const vnode = {
type: 'button',
props: {
disable: false
}
}
```
通过 el.setAttribute 方法来进行设置会遇到的问题:最终渲染出来的按钮就是禁用状态
```js
el.setAttribute('disabled', 'false')
```
解决方案:优先设置 DOM Properties
遇到新的问题:本意是要禁用按钮
```vue
<button disabled>Button</button>
```
```js
const vnode = {
type: 'button',
props: {
disable: ''
}
}
```
```js
el.disabled = ''
```
在对 DOM 的 disabled 属性设置值的时候,任何非布尔类型的值都会被转为布尔类型:
```js
el.disabled = false
```
最终渲染出来的按钮是非禁用状态。
**渲染器内部的实现,不是单独用 HTML Attribute 或者 DOM Properties而是两者结合起来使用并且还会考虑很多的细节以及特殊情况针对特殊情况做特殊处理**。
```js
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 省略 children 的处理
if (vnode.props) {
for (const key in vnode.props) {
// 用 in 操作符判断 key 是否存在对应的 DOM Properties
if (key in el) {
// 获取该 DOM Properties 的类型
const type = typeof el[key];
const value = vnode.props[key];
// 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
if (type === "boolean" && value === "") {
el[key] = true;
} else {
el[key] = value;
}
} else {
// 如果要设置的属性没有对应的 DOM Properties则使用 setAttribute 函数设置属性
el.setAttribute(key, vnode.props[key]);
}
}
}
insert(el, container);
}
```
**2. 只读属性**
```vue
<input form="form1"/>
```
例如 el.form但是这个属性是只读的所以这种情况又只能使用 setAttribute 方法来设置
```js
function shouldSetAsProps(el, key, value) {
// 特殊处理
// 遇到其他特殊情况再进行重构
if (key === "form" && el.tagName === "INPUT") return false;
// 兜底
return key in el;
}
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 省略 children 的处理
if (vnode.props) {
for (const key in vnode.props) {
const value = vnode.props[key];
if (shouldSetAsProps(el, key, value)) {
const type = typeof el[key];
if (type === "boolean" && value === "") {
el[key] = true;
} else {
el[key] = value;
}
} else {
el.setAttribute(key, value);
}
}
}
insert(el, container);
}
```
shouldSetAsProps 这个方法返回一个布尔值,由布尔值来决定是否使用 DOM Properties 来设置。
还可以进一步优化,将属性的设置提取出来:
```js
function shouldSetAsProps(el, key, value) {
// 特殊处理
if (key === "form" && el.tagName === "INPUT") return false;
// 兜底
return key in el;
}
/**
*
* @param {*} el 元素
* @param {*} key 属性
* @param {*} prevValue 旧值
* @param {*} nextValue 新值
*/
function patchProps(el, key, prevValue, nextValue) {
if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key];
if (type === "boolean" && nextValue === "") {
el[key] = true;
} else {
el[key] = nextValue;
}
} else {
el.setAttribute(key, nextValue);
}
}
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 省略 children 的处理
if (vnode.props) {
for (const key in vnode.props) {
// 调用 patchProps 函数即可
patchProps(el, key, null, vnode.props[key]);
}
}
insert(el, container);
}
```
**class处理**
class 本质上也是属性的一种,但是在 Vue 中针对 class 做了增强,因此 Vue 模板中的 class 的值可能会有这么一些情况:
情况一:字符串值
```vue
<template>
<p class="foo bar"></p>
</template>
```
```js
const vnode = {
type: "p",
props: {
class: "foo bar",
},
};
```
情况二:对象值
```vue
<template>
<p :class="cls"></p>
</template>
<script setup>
import { ref } from 'vue'
const cls = ref({
foo: true,
bar: false
})
</script>
```
```js
const vnode = {
type: "p",
props: {
class: { foo: true, bar: false },
},
};
```
情况三:数组值
```vue
<template>
<p :class="arr"></p>
</template>
<script setup>
import { ref } from 'vue'
const arr = ref([
'foo bar',
{
baz: true
}
])
</script>
```
```js
const vnode = {
type: "p",
props: {
class: ["foo bar", { baz: true }],
},
};
```
这里首先第一步就是需要做参数归一化统一成字符串类型。Vue内部有一个方法 normalizeClass 就是做 class 的参数归一化的。
```js
function isString(value) {
return typeof value === "string";
}
function isArray(value) {
return Array.isArray(value);
}
function isObject(value) {
return value !== null && typeof value === "object";
}
function normalizeClass(value) {
let res = "";
if (isString(value)) {
res = value;
} else if (isArray(value)) {
// 如果是数组,递归调用 normalizeClass
for (let i = 0; i < value.length; i++) {
const normalized = normalizeClass(value[i]);
if (normalized) {
res += (res ? " " : "") + normalized;
}
}
} else if (isObject(value)) {
// 如果是对象,则检查每个 key 是否为真值
for (const name in value) {
if (value[name]) {
res += (res ? " " : "") + name;
}
}
}
return res;
}
console.log(normalizeClass("foo")); // 'foo'
console.log(normalizeClass(["foo", "bar"])); // 'foo bar'
console.log(normalizeClass({ foo: true, bar: false })); // 'foo'
console.log(normalizeClass(["foo", { bar: true }])); // 'foo bar'
console.log(normalizeClass(["foo", ["bar", "baz"]])); // 'foo bar baz'
```
```js
const vnode = {
type: "p",
props: {
class: normalizeClass(["foo bar", { baz: true }]),
},
};
```
```js
const vnode = {
type: "p",
props: {
class: 'foo bar baz',
},
};
```
设置class的时候设置方法也有多种
1. setAttribute
2. el.className这种方式效率是最高的
3. el.classList
```js
function patchProps(el, key, prevValue, nextValue) {
// 对 class 进行特殊处理
if (key === "class") {
el.className = nextValue || "";
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key];
if (type === "boolean" && nextValue === "") {
el[key] = true;
} else {
el[key] = nextValue;
}
} else {
el.setAttribute(key, nextValue);
}
}
```
**子节点的挂载**
除了对自身节点的处理,还需要对子节点进行处理,不过处理子节点时涉及到 diff 计算。
```js
function mountElement(vnode, container) {
const el = createElement(vnode.type);
// 针对子节点进行处理
if (typeof vnode.children === "string") {
// 如果 children 是字符串,则直接将字符串插入到元素中
setElementText(el, vnode.children);
} else if (Array.isArray(vnode.children)) {
// 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它们
vnode.children.forEach((child) => {
patch(null, child, el);
});
}
insert(el, container);
}
```
>面试题:说一说渲染器的核心功能是什么?
>
>参考答案:
>
>渲染器最最核心的功能是处理从虚拟 DOM 到真实 DOM 的渲染过程,这个过程包含几个阶段:
>
>1. 挂载:初次渲染时,渲染器会将虚拟 DOM 转化为真实 DOM 并插入页面。它会根据虚拟节点树递归创建 DOM 元素并设置相关属性。
>2. 更新:当组件的状态或属性变化时,渲染器会计算新旧虚拟 DOM 的差异,并通过 Patch 过程最小化更新真实 DOM。
>3. 卸载:当组件被销毁时,渲染器需要将其从 DOM 中移除,并进行必要的清理工作。
>
>每一个步骤都有大量需要考虑的细节,就拿挂载来讲,光是处理元素属性如何挂载就有很多需要考虑的问题,比如:
>
>1. 最终设置属性的时候是用 setAttribute 方法来设置,还是用给 DOM 对象属性赋值的方式来设置
>2. 遇到像 disabled 这样的特殊属性该如何处理
>3. class、style 这样的多值类型,该如何做参数的归一化,归一为哪种形式
>4. 像 class 这样的属性,设置的方式有哪种,哪一种效率高
>
>另外,渲染器和响应式系统是紧密结合在一次的,当组件首次渲染的时候,组件里面的响应式数据会和渲染函数建立依赖关系,当响应式数据发生变化后,渲染函数会重新执行,生成新的虚拟 DOM 树,渲染器随即进入更新阶段,根据新旧两颗虚拟 DOM 树对比来最小化更新真实 DOM这涉及到了 Vue 中的 diff 算法。diff 算法这一块儿Vue2 采用的是双端 diffVue3 则是做了进一步的优化,采用的是快速 diff 算法。diff 这一块儿需要我展开说一下么?
---
-EOF-

View File

@ -0,0 +1,299 @@
# 事件绑定与更新
>面试题:说一下 Vue 内部是如何绑定和更新事件的?
```vue
<p @click="clickHandler">text</p>
```
对应的 vnode 如下:
```js
const vnode = {
type: 'p',
props: {
// 事件其实就是一种特殊的属性放置于props里面
onClick: ()=>{
// ...
}
},
children: 'text'
}
```
所以在渲染器内部可以检测以 on 开头的属性,说明就是事件,例如:
```js
function renderer(vnode, container) {
// 使用 vnode.tag 作为标签名称创建 DOM 元素
const el = document.createElement(vnode.tag);
// 遍历 vnode.props将属性、事件添加到 DOM 元素
for (const key in vnode.props) {
if(/^on/.test(key)){
// 说明是事件
el.addEventListenser(
key.substr(2).toLowerCase(), // 事件名称 onClick --> click
vnode.props[key]
)
}
}
// 处理 children
if (typeof vnode.children === "string") {
// 如果 children 是字符串,说明它是元素的文本子节点
el.appendChild(document.createTextNode(vnode.children));
} else if (Array.isArray(vnode.children)) {
// 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach((child) => renderer(child, el));
}
// 将元素添加到挂载点下
container.appendChild(el);
}
```
不过在 Vue 源码中,渲染器内部其实有一个 patchProps 方法:
```js
function patchProps(el, key, prevValue, nextValue){
if(/^on/.test{key}){
// 说明是事件,做事件的绑定操作
const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
el.addEventListenser(name, vnode.props[key])
} else if(key === 'class'){
// ...
} else if(
//...
){
// ...
}
}
```
如果涉及到事件的更新,则需要先把上一次的**事件卸载**掉,然后绑定新的事件:
```js
function patchProps(el, key, prevValue, nextValue){
if(/^on/.test{key}){
// 说明是事件,做事件的绑定操作
const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
// 移除上一次绑定的事件
prevValue && el.removeEventListenser(name, prevValue);
// 再来绑定新的事件处理函数
el.addEventListenser(name, vnode.props[key])
} else if(key === 'class'){
// ...
} else if(
//...
){
// ...
}
}
```
上面的方式虽然能够正常工作,但是会涉及到反复的绑定和卸载事件。
一种更加优雅的方式是将事件处理器作为一个对象的属性,之后只要更新该对象的属性即可。
```js
function patchProps(el, key, prevValue, nextValue){
if(/^on/.test{key}){
// 说明是事件,做事件的绑定操作
const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
// 这是一个自定义的属性,回头会被赋值为一个函数,该函数会作为事件处理函数
let invoker = el._eventHandler;
if(nextValue){
// 说明有新的事件处理函数
// 这里又有两种情况1. 第一次绑定事件事件的初始化2.非第一次(事件的更新)
if(!invoker){
// 事件的初始化
invoker = el._eventHandler = (e)=>{
// 执行真正的事件处理函数
invoker.value(e)
}
// 将新的事件处理函数挂载 invoker 的 value 属性上面
invoker.value = nextValue;
// 因此是第一次,需要做事件的挂载
el.addEventListenser(name, invoker)
} else {
// 事件的更新
// 更新的时候不需要再像之前一样先卸载事件直接更新invoker的value属性值即可
invoker.value = nextValue;
}
} else {
// 新的事件处理器不存在,那么就需要卸载旧的事件处理器
el.removeEventListenser(name, invoker);
}
} else if(key === 'class'){
// ...
} else if(
//...
){
// ...
}
}
```
不过目前仍然有问题,同一时刻只能缓存一个事件处理函数,而一个元素其实是可以绑定多种事件的,例如:
```js
const vnode = {
type: 'p',
props: {
onClick: ()=>{
// ...
},
onContextmenu: ()=>{
// ...
}
},
children: 'text'
}
```
把 el._eventHandler 由对应的一个函数改为一个对象,对象的键就是事件的名称,对象的值则是对应的事件处理函数:
```js
function patchProps(el, key, prevValue, nextValue){
if(/^on/.test{key}){
// 说明是事件,做事件的绑定操作
const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
// 这是一个自定义的属性,回头会被赋值为一个函数,该函数会作为事件处理函数
const invokers = el._eventHandler || (el._eventHandler = {})
let invoker = invokers[key];
if(nextValue){
// 说明有新的事件处理函数
// 这里又有两种情况1. 第一次绑定事件事件的初始化2.非第一次(事件的更新)
if(!invoker){
// 事件的初始化
invoker = el._eventHandler[key] = (e)=>{
// 执行真正的事件处理函数
invoker.value(e)
}
// 将新的事件处理函数挂载 invoker 的 value 属性上面
invoker.value = nextValue;
// 因此是第一次,需要做事件的挂载
el.addEventListenser(name, invoker)
} else {
// 事件的更新
// 更新的时候不需要再像之前一样先卸载事件直接更新invoker的value属性值即可
invoker.value = nextValue;
}
} else {
// 新的事件处理器不存在,那么就需要卸载旧的事件处理器
el.removeEventListenser(name, invoker);
}
} else if(key === 'class'){
// ...
} else if(
//...
){
// ...
}
}
```
另外还有一种情况我们需要解决,那就是同种事件类型绑定多个事件处理函数的情况,例如:
```js
el.addEventListener('click', fn1);
el.addEventListener('click', fn2);
```
```js
// 对应的 vnode 结构
const vnode = {
type: 'p',
props: {
// 事件其实就是一种特殊的属性放置于props里面
onClick: [
()=>{},
()=>{}
]
},
children: 'text'
}
```
```js
function patchProps(el, key, prevValue, nextValue){
if(/^on/.test{key}){
// 说明是事件,做事件的绑定操作
const name = key.substr(2).toLowerCase(); // 事件名称 onClick --> click
// 这是一个自定义的属性,回头会被赋值为一个函数,该函数会作为事件处理函数
const invokers = el._eventHandler || (el._eventHandler = {})
let invoker = invokers[key];
if(nextValue){
// 说明有新的事件处理函数
// 这里又有两种情况1. 第一次绑定事件事件的初始化2.非第一次(事件的更新)
if(!invoker){
// 事件的初始化
invoker = el._eventHandler[key] = (e)=>{
// 这里需要进行判断,判断是否为数组,如果是数组,说明有多个事件处理函数
if(Array.isArray(invoker.value)){
invoker.value.forEach(fn=>fn(e))
} else {
// 执行真正的事件处理函数
invoker.value(e)
}
}
// 将新的事件处理函数挂载 invoker 的 value 属性上面
invoker.value = nextValue;
// 因此是第一次,需要做事件的挂载
el.addEventListenser(name, invoker)
} else {
// 事件的更新
// 更新的时候不需要再像之前一样先卸载事件直接更新invoker的value属性值即可
invoker.value = nextValue;
}
} else {
// 新的事件处理器不存在,那么就需要卸载旧的事件处理器
el.removeEventListenser(name, invoker);
}
} else if(key === 'class'){
// ...
} else if(
//...
){
// ...
}
}
```
>面试题:说一下 Vue 内部是如何绑定和更新事件的?
>
>参考答案:
>
>开发者在模板中书写事件绑定:
>
>```vue
><p @click='clickHandler'>text</p>
>```
>
>模板被编译器编译后会生成渲染函数,渲染函数的执行得到的是虚拟 DOM.
>
>事件在虚拟 DOM 中其实就是以 Props 的形式存在的。在渲染器内部,会有一个专门针对 Props 进行处理的方法,当遇到以 on 开头的 Prop 时候,会认为这是一个事件,从而进行事件的绑定操作。
>
>为了避免事件更新时频繁的卸载旧事件绑定新事件所带来的性能消耗Vue 内部将事件作为一个对象的属性,更新事件的时候只需要更新对象的属性值即可。该对象的结构大致为:
>
>```js
>{
> onClick: [
> ()=>{},
> ()=>{},
> ],
> onContextmenu: ()=>{}
> // ...
>}
>```
>
>这种结构能做到:
>
>1. 一个元素绑定多种事件
>2. 支持同种事件类型绑定多个事件处理函数
---
-EOF-

View File

@ -0,0 +1,195 @@
# 图解双端diff
>面试题:说一下 Vue3 中的 diff 相较于 Vue2 有什么变化?
- Vue2: 双端diff
- Vue3: 快速diff
**1. diff的概念**
diff 算法是用于比较两棵虚拟 DOM 树的算法,目的是找到它们之间的差异,并根据这些差异高效地更新真实 DOM从而保证页面在数据变化时只进行**最小程度**的 DOM 操作。
思考🤔为什么需要进行diff不是已经有响应式了么
答案:响应式虽然能够侦测到响应式数据的变化,但是只能定位到组件,代表着某一个组件要重新渲染。组件的重新渲染就是重新执行对应的渲染函数,此时就会生成新的虚拟 DOM 树。但是此时我们并不知道新树和旧树具体哪一个节点有区别这个时候就需要diff算法来找到两棵树的区别。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-06-013616.png" alt="20210301193804" style="zoom: 60%;" />
**2. diff算法的特点**
1. 分层对比:它会逐层对比每个节点和它的子节点,避免全树对比,从而提高效率。
2. 相同层级节点对比:在进行 diff 对比的时候Vue会假设对比的节点是同层级的也就是说不会做跨层的比较。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-06-013054.png" alt="20210301203350" style="zoom:65%;" />
**3. diff算法详细流程**
1. 从根节点开始比较,看是否**相同**。所谓相同,是指两个虚拟节点的**标签类型**、**key 值**均相同,但 **input 元素还要看 type 属性**
1. 相同
- 相同就说明能够复用此时就会将旧虚拟DOM节点对应的真实DOM赋值给新虚拟DOM节点
- 对比新节点和旧节点的属性如果属性有变化更新到真实DOM. 这说明了即便是对 DOM 进行复用,也不是完全不处理,还是会有一些针对属性变化的处理
- 进入【对比子节点】
2. 不相同
- 如果不同,该节点以及往下的子节点没有意义了,全部卸载
- 直接根据新虚拟DOM节点递归创建真实DOM同时挂载到新虚拟DOM节点
- 销毁旧虚拟DOM对应的真实DOM背后调用的是 vnode.elm.remove( ) 方法
2. 对比子节点:
1. 仍然是同层做对比
2. 深度优先
3. 同层比较时采用的是双端对比
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-06-021144.png" alt="image-20240906101143754" style="zoom:70%;" />
**4. 双端对比**
之所以被称之为双端,是因为有**两个**指针,一个指向头节点,另一个指向尾节点,如下所示:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-13-145148.png" alt="image-20240913225147579" style="zoom:50%;" />
无论是旧的虚拟 DOM 列表,还是新的虚拟 DOM 列表,都是一头一尾两个指针。
接下来进入比较环节,整体的流程为:
1. 步骤一:新头和旧头比较
- 相同:
- 复用 DOM 节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-021542.png" alt="image-20240914101542039" style="zoom:50%;" />
- 新旧头索引自增
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-021629.png" alt="image-20240914101629244" style="zoom:50%;" />
- 重新开始步骤一
- 不相同:进入步骤二
2. 步骤二:新尾和旧尾比较
- 相同:
- 复用 DOM 节点
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-021834.png" alt="image-20240914101834010" style="zoom:50%;" />
- 新旧尾索引自减
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-021914.png" alt="image-20240914101913347" style="zoom:50%;" />
- 重新开始步骤一
- 不相同,进入步骤三
3. 步骤三:旧头和新尾比较
- 相同:
- 说明可以复用,并且说明节点从头部移动到了尾部,涉及到移动操作,需要将旧头对应的 DOM 节点移动到旧尾对应的 DOM 节点之后
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-021232.png" alt="image-20240914101231300" style="zoom:50%;" />
- 旧头索引自增,新尾索引自减
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-021401.png" alt="image-20240914101400686" style="zoom:50%;" />
- 重新开始步骤一
- 不相同,进入步骤四
4. 步骤四:新头和旧尾比较
- 相同:
- 说明可以复用,并且说明节点从尾部移动到了头部,仍然涉及到移动操作,需要将旧尾对应的 DOM 元素移动到旧头对应的 DOM 节点之前
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-025559.png" alt="image-20240914105559210" style="zoom:50%;" />
- 新头索引自增,旧尾索引自减
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-025649.png" alt="image-20240914105649208" style="zoom:50%;" />
- 重新开始步骤一
- 不相同:进入步骤五
5. 暴力比较:上面 4 个步骤都没找到相同的,则采取暴力比较。在旧节点列表中寻找是否有和新节点相同的节点,
- 找到
- 说明是一个需要移动的节点,将其对应的 DOM 节点移动到旧头对应的 DOM 节点之前
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-030013.png" alt="image-20240914110012627" style="zoom:50%;" />
- 新头索引自增
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-030048.png" alt="image-20240914110048026" style="zoom:50%;" />
- 回到步骤一
- 没找到
- 说明是一个新的节点,创建新的 DOM 节点,插入到旧头对应的 DOM 节点之前
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-030333.png" alt="image-20240914110332605" style="zoom:50%;" />
- 新头索引自增
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-030401.png" alt="image-20240914110401233" style="zoom:50%;" />
- 回到步骤一
新旧节点列表任意一个遍历结束,也就是 oldStart > OldEnd 或者 newStart > newEnd 的时候diff 比较结束。
- 旧节点列表有剩余newStart > newEnd对应的旧 DOM 节点全部删除掉
- 新节点列表有剩余oldStart > OldEnd将新节点列表中剩余的节点创建对应的 DOM放置于新头节点对应的 DOM 节点后面
**综合示例**
当前旧 Vnode 和新 VNode 如下图所示:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-031038.png" alt="image-20240914111038061" style="zoom:50%;" />
1. 头头对比,能够复用,新旧头指针右移
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-031750.png" alt="image-20240914111750328" style="zoom:50%;" />
2. 头头不同,尾尾相同,能够复用,尾尾指针左移
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-031936.png" alt="image-20240914111936261" style="zoom:50%;" />
3. 头头不同尾尾不同旧头新尾相同旧头对应的真实DOM移动到旧尾对应的真实DOM之后旧头索引自增新尾索引自减
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-032233.png" alt="image-20240914112233100" style="zoom:50%;" />
4. 头头不同尾尾不同旧头新尾不同新头旧尾相同旧尾对应的真实DOM移动到旧头对应的真实DOM之前新头索引自增旧尾索引自减
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-032710.png" alt="image-20240914112710405" style="zoom:50%;" />
5. 头头不同尾尾不同旧头新尾不同新头旧尾不同进入暴力对比找到对应节点将对应的真实DOM移动到旧头对应的真实DOM之间新头索引自增
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-033001.png" alt="image-20240914113000896" style="zoom:50%;" />
6. 头头不同尾尾不同旧头新尾不同新头旧尾相同将旧尾对应的真实DOM移动到旧头对应的真实DOM之前新头索引自增旧尾索引自减
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-033248.png" alt="image-20240914113247844" style="zoom:50%;" />
7. 头头不同尾尾不同旧头新尾不同新头旧尾不同暴力对比发现也没找到说明是一个全新的节点创建新的DOM节点插入到旧头对应的DOM节点之前新头索引自增
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-033445.png" alt="image-20240914113444878" style="zoom:50%;" />
8. newEnd > newStartdiff 比对结束,旧 VNode 列表还有剩余,直接删除即可。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-14-033722.png" alt="image-20240914113721337" style="zoom:50%;" />
---
-EOF-

View File

@ -0,0 +1,338 @@
# 最长递增子序列
**基本介绍**
最长递增子序列Longest Increasing Subsequence简称 LIS是计算机科学中一个经典的算法问题。这看上去是很难的一个词语遇到这种词最简单的方法就是拆词这里可以拆为 3 个词:**最长**、**递增**、**子序列**。
1. 子序列
```js
[1, 2, 3, 4, 5]
```
子序列有多个:
```js
[1, 2, 3]
[1, 3]
[2, 4, 5]
```
2. 递增
```js
[2, 1, 5, 3, 6, 4, 8, 9, 7]
```
这个子序列里面的元素必须是递增的:
```js
[1, 5] // 子序列,并且是递增的
[1, 3, 6] // 子序列,并且是递增的
[2, 1, 5] // 子序列,但是不是递增的
```
3. 最长
相当于在上面的基础上,有增加了一个条件,需要是最长的、递增的子序列
```js
[2, 1, 5, 3, 6, 4, 8, 9, 7]
```
最长递增子序列:
```js
[1, 3, 4, 8, 9]
[1, 3, 6, 8, 9]
[1, 5, 6, 8, 9]
[2, 3, 4, 8, 9]
[2, 3, 6, 8, 9]
[2, 5, 6, 8, 9]
```
可以看出,即便是最长递增子序列,仍然是可以有多个的。在开发中,不同的算法可能拿到不一样的结果,不过一般拿到其中一个最长递增子序列即可。
实际意义
- 股票趋势分析
- 手写识别
- 文本编辑和版本控制
- ....
**暴力法**
暴力法的核心思想是:找到所有的递增子序列,然后从中找到长度最长的那一个。
```js
function getSequence(arr) {
let maxLength = 0; // 记录最长递增子序列的长度
let longetSeq = []; // 记录最长递增子序列
/**
*
* @param {*} index 列表的下标
* @param {*} subSeq 当前递增子序列
*/
function findSubsequence(index, subSeq) {
let currentNum = arr[index]; // 当前元素
// 先把之前的递增子序列展开,再加上当前元素
let newSeq = [...subSeq, currentNum]; // 新的递增子序列
// 遍历下标之后的内容
for (let i = index + 1; i < arr.length; i++) {
// 遍历当前下标之后的元素时,发现有比当前元素大的元素
if (arr[i] > currentNum) {
findSubsequence(i, newSeq);
}
}
// 每一次递归结束后,就会得到一个新的递增子序列
// 相当于找到了所有的递增子序列
// console.log("newSeq:", newSeq);
if (newSeq.length > maxLength) {
maxLength = newSeq.length;
longetSeq = newSeq;
}
}
for (let i = 0; i < arr.length; i++) {
findSubsequence(i, []);
}
return longetSeq;
}
const list = [2, 1, 5, 3, 6, 4, 8, 9, 7];
const result = getSequence(list);
console.log(result); // [2, 5, 6, 8, 9]
```
**动态规划**
动态规划Dynamic Programming的核心思想是利用问题的**最优子结构**和**重叠子问题**特性,将复杂问题分解为更小的子问题,并且在解决这些子问题的时候会保存子问题的解,避免重复计算,从而高效地求解原问题。
```js
function getSequence(arr) {
let maxLength = 0; // 记录最长递增子序列的长度
let maxSeq = []; // 记录最长递增子序列
let sequences = new Array(arr.length).fill().map(() => []);
// console.log(sequences);
// 遍历数组
for (let i = 0; i < arr.length; i++) {
// 创建一个以当前元素为结尾的递增子序列
let seq = [arr[i]];
// 遍历之前的元素,找到比当前元素小的元素,从而构建递增子序列
for (let j = 0; j < i; j++) {
if (arr[j] < arr[i]) {
// 把之前存储的序列和当前元素拼接起来
seq = sequences[j].concat(arr[i]);
}
}
// 将当前递增子序列存储起来
sequences[i] = seq;
// 更新最大的序列
if (seq.length > maxLength) {
maxLength = seq.length;
maxSeq = seq;
}
}
// console.log(sequences);
return maxSeq;
}
const list = [2, 1, 5, 3, 6, 4, 8, 9, 7];
const result = getSequence(list);
console.log(result); // [ 1, 3, 4, 8, 9 ]
```
**Vue3中的算法**
Vue3 中获取最长递增子序列,用到了 **贪心****二分** 查找。
```js
function getSequence(arr) {
// 用于记录每个位置的前驱索引,以便最后重建序列
const p = arr.slice();
// 存储当前找到的最长递增子序列的索引
const result = [0];
// 声明循环变量和辅助变量
let i, j, u, v, c;
// 获取输入数组的长度
const len = arr.length;
// 遍历输入数组
for (i = 0; i < len; i++) {
const arrI = arr[i];
// 忽略值为 0 的元素Vue源码中的diff算法对0有特定处理
if (arrI !== 0) {
// 获取当前最长序列中最后一个元素的索引
j = result[result.length - 1];
// 贪心算法部分:如果当前元素大于当前最长序列的最后一个元素,直接添加
if (arr[j] < arrI) {
// 记录当前元素的前驱索引为 j
p[i] = j;
// 将当前元素的索引添加到 result 中
result.push(i);
continue;
}
// 二分查找部分:在 result 中寻找第一个大于等于 arrI 的元素位置
u = 0;
v = result.length - 1;
while (u < v) {
// 取中间位置
c = ((u + v) / 2) | 0;
// 比较中间位置的值与当前值
if (arr[result[c]] < arrI) {
// 如果中间值小于当前值,搜索区间缩小到 [c + 1, v]
u = c + 1;
} else {
// 否则,搜索区间缩小到 [u, c]
v = c;
}
}
// 如果找到的值大于当前值,进行替换
if (arrI < arr[result[u]]) {
// 如果 u 不为 0记录前驱索引
if (u > 0) {
p[i] = result[u - 1];
}
// 更新 result 中的位置 u 为当前索引 i
result[u] = i;
}
}
}
// 重建最长递增子序列
u = result.length;
v = result[u - 1];
while (u-- > 0) {
// 将索引替换为对应的前驱索引
result[u] = v;
v = p[v];
}
// 返回最长递增子序列的索引数组
return result;
}
```
追踪流程:
1. 初始化:
- `p = [2, 1, 5, 3, 6, 4, 8, 9, 7]` 用于记录每个元素的前驱索引,初始为原数组的副本。
- `result = [0]` 初始化结果数组,开始时只包含第一个元素的索引 0。
2. 遍历数组:
- `i = 0, arrI = 2` 第一个元素,索引已在 result 中,继续下一次循环。
- `i = 1, arrI = 1`
- `arr[result[result.length - 1]] = arr[0] = 2`
- `arrI (1) < 2`,需要二分查找替换位置。
- 二分查找 (u = 0, v = 0)
- `c = 0`
- `arr[result[0]] = 2 > arrI (1)`
- `v = c = 0`
- `arrI (1) < arr[result[u]] (2)`,替换 ` result[0] = 1`
- 更新 `result = [1]`
- `i = 2, arrI = 5`
- `arr[result[result.length - 1]] = arr[1] = 1`
- `arrI (5) > 1`,贪心算法:直接添加到 result
- `p[2] = 1`
- `result.push(2)`
- 更新 `result = [1, 2]`
- `i = 3, arrI = 3`
- `arr[result[result.length - 1]] = arr[2] = 5`
- `arrI (3) < 5`,需要二分查找。
- 二分查找 (u = 0, v = 1)
- `c = 0`
- `arr[result[0]] = arr[1] = 1 < arrI (3)`
- `u = c + 1 = 1`
- `arr[result[1]] = arr[2] = 5 > arrI (3)`
- `v = c = 1`
- `arrI (3) < arr[result[u]] (5)`,替换 `result[1] = 3`
- `p[3] = result[0] = 1`
- 更新 `result = [1, 3]`
- `i = 4, arrI = 6`
- `arr[result[result.length - 1]] = arr[3] = 3`
- `arrI (6) > 3`,贪心算法:直接添加到 result
- `p[4] = 3`
- `result.push(4)`
- 更新 `result = [1, 3, 4]`
- `i = 5, arrI = 4`
- `arr[result[result.length - 1]] = arr[4] = 6`
- `arrI (4) < 6`,需要二分查找。
- 二分查找 (u = 0, v = 2)
- `c = 1`
- `arr[result[1]] = arr[3] = 3 < arrI (4)`
- `u = c + 1 = 2`
- `arr[result[2]] = arr[4] = 6 > arrI (4)`
- `v = c = 2`
- `arrI (4) < arr[result[u]] (6)`,替换 `result[2] = 5`
- `p[5] = result[1] = 3`
- 更新 `result = [1, 3, 5]`
- `i = 6, arrI = 8`
- `arr[result[result.length - 1]] = arr[5] = 4`
- `arrI (8) > 4`,贪心算法:直接添加到 result
- `p[6] = 5`
- `result.push(6)`
- 更新 `result = [1, 3, 5, 6]`
- `i = 7, arrI = 9`
- `arr[result[result.length - 1]] = arr[6] = 8`
- `arrI (9) > 8`,贪心算法:直接添加到 `result`
- `p[7] = 6`
- `result.push(7)`
- 更新 `result = [1, 3, 5, 6, 7]`
- `i = 8, arrI = 7`
- `arr[result[result.length - 1]] = arr[7] = 9`
- `arrI (7) < 9`,需要二分查找。
- 二分查找 (u = 0, v = 4)
- `c = 2`
- `arr[result[2]] = arr[5] = 4 < arrI (7)`
- `u = c + 1 = 3`
- `c = 3`
- `arr[result[3]] = arr[6] = 8 > arrI (7)`
- `v = c = 3`
- `arrI (7) < arr[result[u]] (8)`,替换 `result[3] = 8`
- `p[8] = result[2] = 5`
- 更新 `result = [1, 3, 5, 8, 7]`
3. 重建序列:
- `u = result.length = 5`
- `v = result[u - 1] = result[4] = 7`
- 迭代过程:
- `result[4] = v = 7`
- `v = p[7] = 6`
- `result[3] = v = 6`
- `v = p[6] = 5`
- `result[2] = v = 5`
- `v = p[5] = 3`
- `result[1] = v = 3`
- `v = p[3] = 1`
- `result[0] = v = 1`
- `v = p[1]``p[1]` 初始为 1
- 最终 `result = [1, 3, 5, 6, 7]`
4. 映射回原数组的值:
- `result.map(index => list[index])` 得到 `[1, 3, 4, 8, 9]`
- 这是输入数组中的一个最长递增子序列
---
-EOF-

View File

@ -0,0 +1,42 @@
function getSequence(arr) {
let maxLength = 0; // 记录最长递增子序列的长度
let longetSeq = []; // 记录最长递增子序列
/**
*
* @param {*} index 列表的下标
* @param {*} subSeq 当前递增子序列
*/
function findSubsequence(index, subSeq) {
let currentNum = arr[index]; // 当前元素
// 先把之前的递增子序列展开,再加上当前元素
let newSeq = [...subSeq, currentNum]; // 新的递增子序列
// 遍历下标之后的内容
for (let i = index + 1; i < arr.length; i++) {
// 遍历当前下标之后的元素时,发现有比当前元素大的元素
if (arr[i] > currentNum) {
findSubsequence(i, newSeq);
}
}
// 每一次递归结束后,就会得到一个新的递增子序列
// 相当于找到了所有的递增子序列
// console.log("newSeq:", newSeq);
if (newSeq.length > maxLength) {
maxLength = newSeq.length;
longetSeq = newSeq;
}
}
for (let i = 0; i < arr.length; i++) {
findSubsequence(i, []);
}
return longetSeq;
}
const list = [2, 1, 5, 3, 6, 4, 8, 9, 7];
const result = getSequence(list);
console.log(result);

View File

@ -0,0 +1,36 @@
function getSequence(arr) {
let maxLength = 0; // 记录最长递增子序列的长度
let maxSeq = []; // 记录最长递增子序列
let sequences = new Array(arr.length).fill().map(() => []);
// console.log(sequences);
// 遍历数组
for (let i = 0; i < arr.length; i++) {
// 创建一个以当前元素为结尾的递增子序列
let seq = [arr[i]];
// 遍历之前的元素,找到比当前元素小的元素,从而构建递增子序列
for (let j = 0; j < i; j++) {
if (arr[j] < arr[i]) {
// 把之前存储的序列和当前元素拼接起来
seq = sequences[j].concat(arr[i]);
}
}
// 将当前递增子序列存储起来
sequences[i] = seq;
// 更新最大的序列
if (seq.length > maxLength) {
maxLength = seq.length;
maxSeq = seq;
}
}
// console.log(sequences);
return maxSeq;
}
const list = [2, 1, 5, 3, 6, 4, 8, 9, 7];
const result = getSequence(list);
console.log(result); // [ 1, 3, 4, 8, 9 ]

View File

@ -0,0 +1,65 @@
function getSequence(arr) {
// 用于记录每个位置的前驱索引,以便最后重建序列
const p = arr.slice();
// 存储当前找到的最长递增子序列的索引
const result = [0];
// 声明循环变量和辅助变量
let i, j, u, v, c;
// 获取输入数组的长度
const len = arr.length;
// 遍历输入数组
for (i = 0; i < len; i++) {
const arrI = arr[i];
// 忽略值为 0 的元素Vue源码中的diff算法对0有特定处理
if (arrI !== 0) {
// 获取当前最长序列中最后一个元素的索引
j = result[result.length - 1];
// 贪心算法部分:如果当前元素大于当前最长序列的最后一个元素,直接添加
if (arr[j] < arrI) {
// 记录当前元素的前驱索引为 j
p[i] = j;
// 将当前元素的索引添加到 result 中
result.push(i);
continue;
}
// 二分查找部分:在 result 中寻找第一个大于等于 arrI 的元素位置
u = 0;
v = result.length - 1;
while (u < v) {
// 取中间位置
c = ((u + v) / 2) | 0;
// 比较中间位置的值与当前值
if (arr[result[c]] < arrI) {
// 如果中间值小于当前值,搜索区间缩小到 [c + 1, v]
u = c + 1;
} else {
// 否则,搜索区间缩小到 [u, c]
v = c;
}
}
// 如果找到的值大于当前值,进行替换
if (arrI < arr[result[u]]) {
// 如果 u 不为 0记录前驱索引
if (u > 0) {
p[i] = result[u - 1];
}
// 更新 result 中的位置 u 为当前索引 i
result[u] = i;
}
}
}
// 重建最长递增子序列
u = result.length;
v = result[u - 1];
while (u-- > 0) {
// 将索引替换为对应的前驱索引
result[u] = v;
v = p[v];
}
// 返回最长递增子序列的索引数组
return result;
}
const list = [2, 1, 5, 3, 6, 4, 8, 9, 7];
const result = getSequence(list);
console.log(result); // [ 1, 3, 5, 6, 7 ] --> [ 1, 3, 4, 8, 9 ]

View File

@ -0,0 +1,292 @@
# 图解快速diff
>面试题:讲一讲 Vue3 的 diff 算法做了哪些改变?
**双端存在的问题**
在 Vue2 的双端 diff 中,主要的步骤如下:
1. 新头和旧头比较
2. 新尾和旧尾比较
3. 旧头和新尾比较
4. 新头和旧尾比较
5. 暴力对比
这种对比策略其实会存在**额外的移动操作**。
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-16-085545.png" alt="image-20240916165545724" style="zoom:50%;" />
- 对于 e 节点匹配不到,新建 e 节点对应的 DOM 节点,放置于旧头对应的 DOM 节点的前面
- 对于 b 节点,通过暴力比对能够找到,将 b 节点移动到旧头对应的 DOM 节点的前面
- 依此类推c 节点、d 节点所对应的 DOM 节点都会进行移动操作
问题:其实完全不需要移动 bcd 节点,因为在新旧列表里面,这几个节点的顺序是一致的。只需要将 a 节点对应的 DOM 移动到 d 节点后即可。
**Vue3快速diff**
1. 头头比对
2. 尾尾比对
3. 非复杂情况处理
4. 复杂情况处理
**和双端相同步骤**
1. 头头比对
2. 尾尾比对
3. 非复杂情况:指的是经历了头头比对和尾尾比对后,新旧列表有任意一方结束,此时会存在两种情况:
- 旧节点列表有剩余:对应的旧 DOM 节点全部删除
- 新节点列表有剩余:创建对应的 DOM 节点,放置于新头节点对应的 DOM 节点之后
**和双端不同的步骤**
经历了头头比对,尾尾比对后,新旧节点列表都有剩余,之后的步骤就和双端 diff 不一样:
1. 初始化keyToNewIndexMap
2. 初始化newIndexToOldIndexMap
3. 更新newIndexToOldIndexMap
4. 计算最长递增子序列
5. 移动和挂载节点
**1. 初始化keyToNewIndexMap**
首先,定义了一个用于保存新节点下标的容器 keyToNewIndexMap它的形式是 key - index遍历还未处理的新节点将它们的key和下标的映射关系存储到 keyToNewIndexMap 中。
```js
const keyToNewIndexMap = new Map();
for (let i = newStartIdx; i <= newEndIdx; i++) {
const key = newChildren[i].key;
keyToNewIndexMap.set(key, i);
}
```
示意图:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-17-004920.png" alt="image-20240917084919424" style="zoom:50%;" />
也就是说,该 map 存储了所有未处理的新节点的 key 和 index 的映射关系。
**2. 初始化newIndexToOldIndexMap**
然后,定义了一个和未处理新节点个数同样大小的数组**newIndexToOldIndexMap**,默认每一项均为 0
```js
const toBePatched = newEndIdx - newStartIdx + 1; // 计算没有处理的新节点的个数
const newIndexToOldIndexMap = new Array(toBePatched).fill(0);
```
示意图:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-17-064415.png" alt="image-20240917144414276" style="zoom:50%;" />
之所以一开始初始化为 0 ,其实是为了一开始假设新节点不存在于旧节点列表,之后就会对这个数组进行更新,倘若更新之后当前某个位置还为 0 ,就代表这一位对应的新节点在旧节点列表中不存在。
**3. 更新newIndexToOldIndexMap**
遍历未处理的**旧节点**,查找旧节点在新节点中的位置,决定是更新、删除还是移动。
- 遍历未处理的旧节点(从 oldStartIdx 到 oldEndIdx
- 对于每个旧节点,执行以下操作:
- 查找对应的新节点索引 newIndex
- 如果旧节点有 key通过 keyToNewIndexMap 获取 newIndex
- 如果没有 key需要遍历新节点列表找到第一个与旧节点相同的节点
- 判断节点是否存在与新节点列表:
- 如果 newIndex 没有找到,说明旧节点已经被删除,需要卸载
- 如果 newIndex 找到,说明节点需要保留,执行以下操作:
- 更新节点:调用 patch 函数更新节点内容
- 记录映射关系:将旧节点的索引 +1 记录到 `newIndexToOldIndexMap[newIndex - newStartIdx]`
>思考🤔:为什么要把旧节点的索引 +1 然后进行存储?
>
>答案因为前面我们在初始化newIndexToOldIndexMap这个数组的时候所有的值都初始化为了0代表新节点在旧节点列表中不存在。如果直接存储旧节点的索引而恰好这个旧节点的索引又为0那么此时是无法区分究竟是索引值还是不存在。
- 标记节点是否需要移动:通过比较当前的遍历顺序和 newIndex初步判断节点是否需要移动。
示意代码:
```js
let moved = false;
let maxNewIndexSoFar = 0;
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const oldNode = oldChildren[i];
let newIndex;
if (oldNode.key != null) {
// 旧节点存在 key根据 key 找到该节点在新节点列表里面的索引值
newIndex = keyToNewIndexMap.get(oldNode.key);
} else {
// 遍历新节点列表匹配
}
if (newIndex === undefined) {
// 旧节点在新节点中不存在,卸载
} else {
// 更新节点
patch(oldNode, newChildren[newIndex], container);
// 记录映射关系注意这里在记录的时候旧节点的索引要加1
newIndexToOldIndexMap[newIndex - newStartIdx] = i + 1;
// 判断是否需要移动
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}
}
}
```
详细步骤:
- i = 0`[0, 0, 0, 0, 1, 0]`
- i = 1`[0, 2, 0, 0, 1, 0]`
- i = 2`[0, 2, 3, 0, 1, 0]`
- i = 3`[0, 2, 3, 4, 1, 0]`
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2024-09-17-065254.png" alt="image-20240917145254007" style="zoom:50%;" />
经过遍历旧节点列表这一操作之后newIndexToOldIndexMap 就被更新,里面存储了每个新节点在旧节点列表里面的位置,不过要注意,这个索引位置是 +1. 更新后如果某一项仍然是 0说明这一个节点确实在旧节点列表中不存在
```js
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}
```
maxNewIndexSoFar 用于判断节点的相对顺序是否保持递增,以决定是否需要移动节点。
- 如果当前的新节点索引大于等于 maxNewIndexSoFar更新 maxNewIndexSoFar节点相对顺序正确无需标记移动
- 如果小于,说明节点相对顺序发生变化,标记 moved = true后续需要根据 LIS 决定是否移动节点。
**4. 计算最长递增子序列**
通过 LIS确定哪些节点的相对顺序未变减少需要移动的节点数量。如果在前面的步骤中标记了 moved = true说明有节点需要移动。使用 newIndexToOldIndexMap 计算最长递增子序列 increasingNewIndexSequence.
```js
const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : [];
```
上一步我们得到的 newIndexToOldIndex 为 `[0, 2, 3, 4, 1, 0]`,之后得到的最长递增子序列为 `[1, 2, 3]`注意Vue3内部在计算最长递增子序列的时候返回的是元素对应的索引值。
思考🤔:注意这里的最长递增子序列不是记录的具体元素,而是元素对应的下标值。这样有什么好处?
答案:这样刚好抵消了前面+1的操作重新变回了旧节点的下标。
**5. 移动和挂载节点**
根据计算结果,对需要移动和新建的节点进行处理。**倒序遍历**未处理的新节点。
思考🤔:为什么要倒序遍历?
答案:因为后续的节点位置是确定了的,通过倒序的方式能够避免锚点引用的时候不会出错。
具体步骤:
1. 计算当前新节点在新节点列表中的索引 newIndex = newStartIdx + i
- newStartIdx 是未处理节点的起始索引
- i 为倒序遍历时的索引值
2. 获取锚点 DOM其目的是为了作为节点移动的参照物当涉及到移动操作时都移动到锚点 DOM 的前面
- 计算方法为 `newIndex + 1 < newChildren.length ? newChildren[newIndex + 1].el : null`
- 如果计算出来为 null表示没有对应的锚点 DOM ,那么就创建并挂载到最后
3. 判断节点究竟是新挂载还是移动
- **判断节点是否需要挂载**:如果 `newIndexToOldIndexMap[i] === 0`说明该节点在旧节点中不存在需要创建并插入到锚点DOM位置之前。
```js
if (newIndexToOldIndexMap[i] === 0) {
// 创建新节点并插入到锚点DOM位置之前
patch(/*参数略 */);
}
```
- **判断节点是否需要移动**:如果节点在 increasingNewIndexSequence 中说明位置正确无需移动。如果不在则需要移动节点到锚点DOM位置之前。
```js
else if (moved) {
if (!increasingNewIndexSequence.includes(i)) {
// 移动节点到锚点DOM之前
move(/*参数略 */);
}
}
```
详细步骤:
- i = 5
- newIndex = 5
- 锚点DOMnull
- 创建 m 对应的真实 DOM挂载到最后
- i = 4
- newIndex = 4
- 锚点DOMm --> 真实DOM
- `newIndexToOldIndexMap[4]` 是否为 0不是说明在旧节点列表里面是有的能够复用
- 接下来看 i 是否在最长递增子序列里面发现没有在最长递增子序列里面那么这里就涉及到移动移动到锚点DOM的前面也就是 m 前面
- i = 3
- newIndex = 3
- 锚点DOMa --> 真实DOM
- `newIndexToOldIndexMap[3]` 不为0说明旧节点列表里面是有的能够复用
- 接下来需要看 i 是否在最长递增子序列里面,发现存在,所以不做任何操作
- i = 2
- newIndex = 2
- 锚点DOMd --> 真实DOM
- `newIndexToOldIndexMap[2]` 不为0说明旧节点列表里面是有的能够复用
- 接下来需要看 i 是否在最长递增子序列里面,发现存在,所以不做任何操作
- i = 1
- newIndex = 1
- 锚点DOMc --> 真实DOM
- `newIndexToOldIndexMap[1]` 不为0说明旧节点列表里面是有的能够复用
- 接下来需要看 i 是否在最长递增子序列里面,发现存在,所以不做任何操作
- i = 0
- newIndex = 0
- 锚点DOMb --> 真实DOM
- `newIndexToOldIndexMap[0]` 为0说明旧节点列表里面没有
- 创建新的 DOM 节点,插入到锚点 DOM 节点之前
最终经过上面的操作:
1. e新建并且插入到 b 之前
2. b 位置不变,没有做移动操作
3. c位置不变没有做移动操作
4. d位置不变没有做移动操作
5. a移动到 m 之前
6. m新建并且插入到末尾
整个 diff 下来 DOM 操作仅仅有 1 次移动2 次新建。做到了最最最小化 DOM 操作次数,没有一次 DOM 操作是多余的。
>面试题:讲一讲 Vue3 的 diff 算法做了哪些改变?
>
>参考答案:
>
>Vue2 采用的是双端 diff 算法,而 Vue3 采用的是快速 diff. 这两种 diff 算法前面的步骤都是相同的,先是新旧列表的头节点进行比较,当发现无法复用则进行新旧节点列表的尾节点比较。
>
>一头一尾比较完后,如果旧节点列表有剩余,就将对应的旧 DOM 节点全部删除掉,如果新节点列表有剩余:将新节点列表中剩余的节点创建对应的 DOM放置于新头节点对应的 DOM 节点后面。
>
>之后两种 diff 算法呈现出不同的操作,双端会进行旧头新尾比较、无法复用则进行旧尾新头比较、再无法复用这是暴力比对,这样的处理会存在多余的移动操作,即便一些新节点的前后顺序和旧节点是一致的,但是还是会产生移动操作。
>
>而 Vue3 快速 diff 则采用了另外一种做法,找到新节点在旧节点中对应的索引列表,然后求出最长递增子序列,凡是位于最长递增子序列里面的索引所对应的元素,是不需要移动位置的,这就做到了只移动需要移动的 DOM 节点,最小化了 DOM 的操作次数没有任何无意义的移动。可以这么说Vue3 的 diff 再一次将性能优化到了极致,整套操作下来,没有一次 DOM 操作是多余的,仅仅执行了最必要的 DOM 操作。
---
-EOF-

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

Some files were not shown because too many files have changed in this diff Show More