diff --git a/08. 渲染器核心功能/课件资料/渲染器核心功能.md b/08. 渲染器核心功能/课件资料/渲染器核心功能.md new file mode 100644 index 0000000..bc7c73c --- /dev/null +++ b/08. 渲染器核心功能/课件资料/渲染器核心功能.md @@ -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 + +``` + +**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 + +``` + +vnode: + +```js +const vnode = { + type: 'button', + props: { + disable: false + } +} +``` + +通过 el.setAttribute 方法来进行设置会遇到的问题:最终渲染出来的按钮就是禁用状态 + +```js + el.setAttribute('disabled', 'false') +``` + +解决方案:优先设置 DOM Properties + +遇到新的问题:本意是要禁用按钮 + +```vue + +``` + +```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 + +``` + +例如 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 + +``` + +```js +const vnode = { + type: "p", + props: { + class: "foo bar", + }, +}; +``` + +情况二:对象值 + +```vue + + +``` + +```js +const vnode = { + type: "p", + props: { + class: { foo: true, bar: false }, + }, +}; +``` + +情况三:数组值 + +```vue + + +``` + +```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 采用的是双端 diff,Vue3 则是做了进一步的优化,采用的是快速 diff 算法。diff 这一块儿需要我展开说一下么? + +--- + +-EOF- \ No newline at end of file diff --git a/09. 事件绑定与更新/课件资料/事件绑定与更新.md b/09. 事件绑定与更新/课件资料/事件绑定与更新.md new file mode 100644 index 0000000..c41e753 --- /dev/null +++ b/09. 事件绑定与更新/课件资料/事件绑定与更新.md @@ -0,0 +1,299 @@ +# 事件绑定与更新 + +>面试题:说一下 Vue 内部是如何绑定和更新事件的? + +```vue +

text

+``` + +对应的 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 +>

text

+>``` +> +>模板被编译器编译后会生成渲染函数,渲染函数的执行得到的是虚拟 DOM. +> +>事件在虚拟 DOM 中其实就是以 Props 的形式存在的。在渲染器内部,会有一个专门针对 Props 进行处理的方法,当遇到以 on 开头的 Prop 时候,会认为这是一个事件,从而进行事件的绑定操作。 +> +>为了避免事件更新时频繁的卸载旧事件,绑定新事件所带来的性能消耗,Vue 内部将事件作为一个对象的属性,更新事件的时候只需要更新对象的属性值即可。该对象的结构大致为: +> +>```js +>{ +> onClick: [ +> ()=>{}, +> ()=>{}, +> ], +> onContextmenu: ()=>{} +> // ... +>} +>``` +> +>这种结构能做到: +> +>1. 一个元素绑定多种事件 +>2. 支持同种事件类型绑定多个事件处理函数 + +--- + +-EOF- \ No newline at end of file diff --git a/12. 图解双端diff/课件资料/图解双端diff.md b/12. 图解双端diff/课件资料/图解双端diff.md new file mode 100644 index 0000000..bafbb5c --- /dev/null +++ b/12. 图解双端diff/课件资料/图解双端diff.md @@ -0,0 +1,195 @@ +# 图解双端diff + +>面试题:说一下 Vue3 中的 diff 相较于 Vue2 有什么变化? + +- Vue2: 双端diff +- Vue3: 快速diff + +**1. diff的概念** + +diff 算法是用于比较两棵虚拟 DOM 树的算法,目的是找到它们之间的差异,并根据这些差异高效地更新真实 DOM,从而保证页面在数据变化时只进行**最小程度**的 DOM 操作。 + +思考🤔:为什么需要进行diff,不是已经有响应式了么? + +答案:响应式虽然能够侦测到响应式数据的变化,但是只能定位到组件,代表着某一个组件要重新渲染。组件的重新渲染就是重新执行对应的渲染函数,此时就会生成新的虚拟 DOM 树。但是此时我们并不知道新树和旧树具体哪一个节点有区别,这个时候就需要diff算法来找到两棵树的区别。 + +20210301193804 + +**2. diff算法的特点** + +1. 分层对比:它会逐层对比每个节点和它的子节点,避免全树对比,从而提高效率。 +2. 相同层级节点对比:在进行 diff 对比的时候,Vue会假设对比的节点是同层级的,也就是说,不会做跨层的比较。 + +20210301203350 + +**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. 同层比较时采用的是双端对比 + + image-20240906101143754 + + + +**4. 双端对比** + +之所以被称之为双端,是因为有**两个**指针,一个指向头节点,另一个指向尾节点,如下所示: + +image-20240913225147579 + +无论是旧的虚拟 DOM 列表,还是新的虚拟 DOM 列表,都是一头一尾两个指针。 + +接下来进入比较环节,整体的流程为: + +1. 步骤一:新头和旧头比较 + + - 相同: + + - 复用 DOM 节点 + + image-20240914101542039 + + - 新旧头索引自增 + + image-20240914101629244 + + - 重新开始步骤一 + + - 不相同:进入步骤二 + +2. 步骤二:新尾和旧尾比较 + + - 相同: + + - 复用 DOM 节点 + + image-20240914101834010 + + - 新旧尾索引自减 + + image-20240914101913347 + + - 重新开始步骤一 + + - 不相同,进入步骤三 + +3. 步骤三:旧头和新尾比较 + + - 相同: + + - 说明可以复用,并且说明节点从头部移动到了尾部,涉及到移动操作,需要将旧头对应的 DOM 节点移动到旧尾对应的 DOM 节点之后 + + image-20240914101231300 + + - 旧头索引自增,新尾索引自减 + + image-20240914101400686 + + - 重新开始步骤一 + + - 不相同,进入步骤四 + +4. 步骤四:新头和旧尾比较 + + - 相同: + + - 说明可以复用,并且说明节点从尾部移动到了头部,仍然涉及到移动操作,需要将旧尾对应的 DOM 元素移动到旧头对应的 DOM 节点之前 + + image-20240914105559210 + + - 新头索引自增,旧尾索引自减 + + image-20240914105649208 + + - 重新开始步骤一 + + - 不相同:进入步骤五 + +5. 暴力比较:上面 4 个步骤都没找到相同的,则采取暴力比较。在旧节点列表中寻找是否有和新节点相同的节点, + + - 找到 + + - 说明是一个需要移动的节点,将其对应的 DOM 节点移动到旧头对应的 DOM 节点之前 + + image-20240914110012627 + + - 新头索引自增 + + image-20240914110048026 + + - 回到步骤一 + + - 没找到 + + - 说明是一个新的节点,创建新的 DOM 节点,插入到旧头对应的 DOM 节点之前 + + image-20240914110332605 + + - 新头索引自增 + + image-20240914110401233 + + - 回到步骤一 + +新旧节点列表任意一个遍历结束,也就是 oldStart > OldEnd 或者 newStart > newEnd 的时候,diff 比较结束。 + +- 旧节点列表有剩余(newStart > newEnd):对应的旧 DOM 节点全部删除掉 +- 新节点列表有剩余(oldStart > OldEnd):将新节点列表中剩余的节点创建对应的 DOM,放置于新头节点对应的 DOM 节点后面 + + + +**综合示例** + +当前旧 Vnode 和新 VNode 如下图所示: + +image-20240914111038061 + +1. 头头对比,能够复用,新旧头指针右移 + + image-20240914111750328 + +2. 头头不同,尾尾相同,能够复用,尾尾指针左移 + + image-20240914111936261 + +3. 头头不同,尾尾不同,旧头新尾相同,旧头对应的真实DOM移动到旧尾对应的真实DOM之后,旧头索引自增,新尾索引自减 + + image-20240914112233100 + +4. 头头不同,尾尾不同,旧头新尾不同,新头旧尾相同,旧尾对应的真实DOM移动到旧头对应的真实DOM之前,新头索引自增,旧尾索引自减 + + image-20240914112710405 + +5. 头头不同,尾尾不同,旧头新尾不同,新头旧尾不同,进入暴力对比,找到对应节点,将对应的真实DOM移动到旧头对应的真实DOM之间,新头索引自增 + + image-20240914113000896 + +6. 头头不同,尾尾不同,旧头新尾不同,新头旧尾相同,将旧尾对应的真实DOM移动到旧头对应的真实DOM之前,新头索引自增,旧尾索引自减 + + image-20240914113247844 + +7. 头头不同,尾尾不同,旧头新尾不同,新头旧尾不同,暴力对比发现也没找到,说明是一个全新的节点,创建新的DOM节点,插入到旧头对应的DOM节点之前,新头索引自增 + + image-20240914113444878 + +8. newEnd > newStart,diff 比对结束,旧 VNode 列表还有剩余,直接删除即可。 + + image-20240914113721337 + +--- + +-EOF- \ No newline at end of file diff --git a/13. 最长递增子序列/课件资料/最长递增子序列.md b/13. 最长递增子序列/课件资料/最长递增子序列.md new file mode 100644 index 0000000..2b7f5e7 --- /dev/null +++ b/13. 最长递增子序列/课件资料/最长递增子序列.md @@ -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- \ No newline at end of file diff --git a/13. 最长递增子序列/课堂代码/1.暴力法.js b/13. 最长递增子序列/课堂代码/1.暴力法.js new file mode 100644 index 0000000..74da46e --- /dev/null +++ b/13. 最长递增子序列/课堂代码/1.暴力法.js @@ -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); diff --git a/13. 最长递增子序列/课堂代码/2.动态规划.js b/13. 最长递增子序列/课堂代码/2.动态规划.js new file mode 100644 index 0000000..80da0c8 --- /dev/null +++ b/13. 最长递增子序列/课堂代码/2.动态规划.js @@ -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 ] diff --git a/13. 最长递增子序列/课堂代码/3.Vue3算法.js b/13. 最长递增子序列/课堂代码/3.Vue3算法.js new file mode 100644 index 0000000..c12acc8 --- /dev/null +++ b/13. 最长递增子序列/课堂代码/3.Vue3算法.js @@ -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 ] \ No newline at end of file diff --git a/14. 图解快速diff/图解快速diff.md b/14. 图解快速diff/图解快速diff.md new file mode 100644 index 0000000..2652a3c --- /dev/null +++ b/14. 图解快速diff/图解快速diff.md @@ -0,0 +1,292 @@ +# 图解快速diff + +>面试题:讲一讲 Vue3 的 diff 算法做了哪些改变? + +**双端存在的问题** + +在 Vue2 的双端 diff 中,主要的步骤如下: + +1. 新头和旧头比较 +2. 新尾和旧尾比较 +3. 旧头和新尾比较 +4. 新头和旧尾比较 +5. 暴力对比 + +这种对比策略其实会存在**额外的移动操作**。 + +image-20240916165545724 + +- 对于 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); +} +``` + +示意图: + +image-20240917084919424 + +也就是说,该 map 存储了所有未处理的新节点的 key 和 index 的映射关系。 + + + +**2. 初始化newIndexToOldIndexMap** + +然后,定义了一个和未处理新节点个数同样大小的数组**newIndexToOldIndexMap**,默认每一项均为 0 + +```js +const toBePatched = newEndIdx - newStartIdx + 1; // 计算没有处理的新节点的个数 +const newIndexToOldIndexMap = new Array(toBePatched).fill(0); +``` + +示意图: + +image-20240917144414276 + +之所以一开始初始化为 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]` + +image-20240917145254007 + +经过遍历旧节点列表这一操作之后,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 + - 锚点DOM:null + - 创建 m 对应的真实 DOM,挂载到最后 +- i = 4 + - newIndex = 4 + - 锚点DOM:m --> 真实DOM + - `newIndexToOldIndexMap[4]` 是否为 0,不是说明在旧节点列表里面是有的,能够复用 + - 接下来看 i 是否在最长递增子序列里面,发现没有在最长递增子序列里面,那么这里就涉及到移动,移动到锚点DOM的前面,也就是 m 前面 +- i = 3 + - newIndex = 3 + - 锚点DOM:a --> 真实DOM + - `newIndexToOldIndexMap[3]` 不为0,说明旧节点列表里面是有的,能够复用 + - 接下来需要看 i 是否在最长递增子序列里面,发现存在,所以不做任何操作 +- i = 2 + - newIndex = 2 + - 锚点DOM:d --> 真实DOM + - `newIndexToOldIndexMap[2]` 不为0,说明旧节点列表里面是有的,能够复用 + - 接下来需要看 i 是否在最长递增子序列里面,发现存在,所以不做任何操作 +- i = 1 + - newIndex = 1 + - 锚点DOM:c --> 真实DOM + - `newIndexToOldIndexMap[1]` 不为0,说明旧节点列表里面是有的,能够复用 + - 接下来需要看 i 是否在最长递增子序列里面,发现存在,所以不做任何操作 +- i = 0 + - newIndex = 0 + - 锚点DOM:b --> 真实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- \ No newline at end of file diff --git a/14. 模板编译器part1/课件资料/模板编译器.md b/15. 模板编译器part1/课件资料/模板编译器.md similarity index 100% rename from 14. 模板编译器part1/课件资料/模板编译器.md rename to 15. 模板编译器part1/课件资料/模板编译器.md diff --git a/14. 模板编译器part1/课堂代码/demo/.vscode/settings.json b/15. 模板编译器part1/课堂代码/demo/.vscode/settings.json similarity index 100% rename from 14. 模板编译器part1/课堂代码/demo/.vscode/settings.json rename to 15. 模板编译器part1/课堂代码/demo/.vscode/settings.json diff --git a/14. 模板编译器part1/课堂代码/demo/index.html b/15. 模板编译器part1/课堂代码/demo/index.html similarity index 100% rename from 14. 模板编译器part1/课堂代码/demo/index.html rename to 15. 模板编译器part1/课堂代码/demo/index.html diff --git a/15. 模板编译器part2/课件资料/模板编译器.md b/16. 模板编译器part2/课件资料/模板编译器.md similarity index 100% rename from 15. 模板编译器part2/课件资料/模板编译器.md rename to 16. 模板编译器part2/课件资料/模板编译器.md diff --git a/15. 模板编译器part2/课堂代码/demo/.vscode/settings.json b/16. 模板编译器part2/课堂代码/demo/.vscode/settings.json similarity index 100% rename from 15. 模板编译器part2/课堂代码/demo/.vscode/settings.json rename to 16. 模板编译器part2/课堂代码/demo/.vscode/settings.json diff --git a/15. 模板编译器part2/课堂代码/demo/index.html b/16. 模板编译器part2/课堂代码/demo/index.html similarity index 100% rename from 15. 模板编译器part2/课堂代码/demo/index.html rename to 16. 模板编译器part2/课堂代码/demo/index.html diff --git a/16. 模板编译器part3/课件资料/模板编译器.md b/17. 模板编译器part3/课件资料/模板编译器.md similarity index 100% rename from 16. 模板编译器part3/课件资料/模板编译器.md rename to 17. 模板编译器part3/课件资料/模板编译器.md diff --git a/16. 模板编译器part3/课堂代码/demo/.vscode/settings.json b/17. 模板编译器part3/课堂代码/demo/.vscode/settings.json similarity index 100% rename from 16. 模板编译器part3/课堂代码/demo/.vscode/settings.json rename to 17. 模板编译器part3/课堂代码/demo/.vscode/settings.json diff --git a/16. 模板编译器part3/课堂代码/demo/index.html b/17. 模板编译器part3/课堂代码/demo/index.html similarity index 100% rename from 16. 模板编译器part3/课堂代码/demo/index.html rename to 17. 模板编译器part3/课堂代码/demo/index.html diff --git a/17. 模板编译器part4/课件资料/模板编译器.md b/18. 模板编译器part4/课件资料/模板编译器.md similarity index 100% rename from 17. 模板编译器part4/课件资料/模板编译器.md rename to 18. 模板编译器part4/课件资料/模板编译器.md diff --git a/17. 模板编译器part4/课堂代码/demo/.vscode/settings.json b/18. 模板编译器part4/课堂代码/demo/.vscode/settings.json similarity index 100% rename from 17. 模板编译器part4/课堂代码/demo/.vscode/settings.json rename to 18. 模板编译器part4/课堂代码/demo/.vscode/settings.json diff --git a/17. 模板编译器part4/课堂代码/demo/index.html b/18. 模板编译器part4/课堂代码/demo/index.html similarity index 100% rename from 17. 模板编译器part4/课堂代码/demo/index.html rename to 18. 模板编译器part4/课堂代码/demo/index.html diff --git a/18. 模板编译优化/课件资料/模板编译提升.md b/19. 模板编译优化/课件资料/模板编译提升.md similarity index 100% rename from 18. 模板编译优化/课件资料/模板编译提升.md rename to 19. 模板编译优化/课件资料/模板编译提升.md diff --git a/18. 模板编译优化/课堂代码/demo/.gitignore b/19. 模板编译优化/课堂代码/demo/.gitignore similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/.gitignore rename to 19. 模板编译优化/课堂代码/demo/.gitignore diff --git a/18. 模板编译优化/课堂代码/demo/.vscode/extensions.json b/19. 模板编译优化/课堂代码/demo/.vscode/extensions.json similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/.vscode/extensions.json rename to 19. 模板编译优化/课堂代码/demo/.vscode/extensions.json diff --git a/18. 模板编译优化/课堂代码/demo/README.md b/19. 模板编译优化/课堂代码/demo/README.md similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/README.md rename to 19. 模板编译优化/课堂代码/demo/README.md diff --git a/18. 模板编译优化/课堂代码/demo/index.html b/19. 模板编译优化/课堂代码/demo/index.html similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/index.html rename to 19. 模板编译优化/课堂代码/demo/index.html diff --git a/18. 模板编译优化/课堂代码/demo/jsconfig.json b/19. 模板编译优化/课堂代码/demo/jsconfig.json similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/jsconfig.json rename to 19. 模板编译优化/课堂代码/demo/jsconfig.json diff --git a/18. 模板编译优化/课堂代码/demo/package-lock.json b/19. 模板编译优化/课堂代码/demo/package-lock.json similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/package-lock.json rename to 19. 模板编译优化/课堂代码/demo/package-lock.json diff --git a/18. 模板编译优化/课堂代码/demo/package.json b/19. 模板编译优化/课堂代码/demo/package.json similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/package.json rename to 19. 模板编译优化/课堂代码/demo/package.json diff --git a/18. 模板编译优化/课堂代码/demo/public/favicon.ico b/19. 模板编译优化/课堂代码/demo/public/favicon.ico similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/public/favicon.ico rename to 19. 模板编译优化/课堂代码/demo/public/favicon.ico diff --git a/18. 模板编译优化/课堂代码/demo/src/App.vue b/19. 模板编译优化/课堂代码/demo/src/App.vue similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/App.vue rename to 19. 模板编译优化/课堂代码/demo/src/App.vue diff --git a/18. 模板编译优化/课堂代码/demo/src/assets/base.css b/19. 模板编译优化/课堂代码/demo/src/assets/base.css similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/assets/base.css rename to 19. 模板编译优化/课堂代码/demo/src/assets/base.css diff --git a/18. 模板编译优化/课堂代码/demo/src/assets/logo.svg b/19. 模板编译优化/课堂代码/demo/src/assets/logo.svg similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/assets/logo.svg rename to 19. 模板编译优化/课堂代码/demo/src/assets/logo.svg diff --git a/18. 模板编译优化/课堂代码/demo/src/assets/main.css b/19. 模板编译优化/课堂代码/demo/src/assets/main.css similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/assets/main.css rename to 19. 模板编译优化/课堂代码/demo/src/assets/main.css diff --git a/18. 模板编译优化/课堂代码/demo/src/components/HelloWorld.vue b/19. 模板编译优化/课堂代码/demo/src/components/HelloWorld.vue similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/components/HelloWorld.vue rename to 19. 模板编译优化/课堂代码/demo/src/components/HelloWorld.vue diff --git a/18. 模板编译优化/课堂代码/demo/src/components/TheWelcome.vue b/19. 模板编译优化/课堂代码/demo/src/components/TheWelcome.vue similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/components/TheWelcome.vue rename to 19. 模板编译优化/课堂代码/demo/src/components/TheWelcome.vue diff --git a/18. 模板编译优化/课堂代码/demo/src/components/WelcomeItem.vue b/19. 模板编译优化/课堂代码/demo/src/components/WelcomeItem.vue similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/components/WelcomeItem.vue rename to 19. 模板编译优化/课堂代码/demo/src/components/WelcomeItem.vue diff --git a/18. 模板编译优化/课堂代码/demo/src/components/icons/IconCommunity.vue b/19. 模板编译优化/课堂代码/demo/src/components/icons/IconCommunity.vue similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/components/icons/IconCommunity.vue rename to 19. 模板编译优化/课堂代码/demo/src/components/icons/IconCommunity.vue diff --git a/18. 模板编译优化/课堂代码/demo/src/components/icons/IconDocumentation.vue b/19. 模板编译优化/课堂代码/demo/src/components/icons/IconDocumentation.vue similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/components/icons/IconDocumentation.vue rename to 19. 模板编译优化/课堂代码/demo/src/components/icons/IconDocumentation.vue diff --git a/18. 模板编译优化/课堂代码/demo/src/components/icons/IconEcosystem.vue b/19. 模板编译优化/课堂代码/demo/src/components/icons/IconEcosystem.vue similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/components/icons/IconEcosystem.vue rename to 19. 模板编译优化/课堂代码/demo/src/components/icons/IconEcosystem.vue diff --git a/18. 模板编译优化/课堂代码/demo/src/components/icons/IconSupport.vue b/19. 模板编译优化/课堂代码/demo/src/components/icons/IconSupport.vue similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/components/icons/IconSupport.vue rename to 19. 模板编译优化/课堂代码/demo/src/components/icons/IconSupport.vue diff --git a/18. 模板编译优化/课堂代码/demo/src/components/icons/IconTooling.vue b/19. 模板编译优化/课堂代码/demo/src/components/icons/IconTooling.vue similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/components/icons/IconTooling.vue rename to 19. 模板编译优化/课堂代码/demo/src/components/icons/IconTooling.vue diff --git a/18. 模板编译优化/课堂代码/demo/src/main.js b/19. 模板编译优化/课堂代码/demo/src/main.js similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/src/main.js rename to 19. 模板编译优化/课堂代码/demo/src/main.js diff --git a/18. 模板编译优化/课堂代码/demo/vite.config.js b/19. 模板编译优化/课堂代码/demo/vite.config.js similarity index 100% rename from 18. 模板编译优化/课堂代码/demo/vite.config.js rename to 19. 模板编译优化/课堂代码/demo/vite.config.js diff --git a/19. 组件name作用/课件资料/组件name作用.md b/20. 组件name作用/课件资料/组件name作用.md similarity index 100% rename from 19. 组件name作用/课件资料/组件name作用.md rename to 20. 组件name作用/课件资料/组件name作用.md diff --git a/19. 组件name作用/课堂代码/1. FolderTree/.eslintrc.cjs b/20. 组件name作用/课堂代码/1. FolderTree/.eslintrc.cjs similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/.eslintrc.cjs rename to 20. 组件name作用/课堂代码/1. FolderTree/.eslintrc.cjs diff --git a/19. 组件name作用/课堂代码/1. FolderTree/.gitignore b/20. 组件name作用/课堂代码/1. FolderTree/.gitignore similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/.gitignore rename to 20. 组件name作用/课堂代码/1. FolderTree/.gitignore diff --git a/19. 组件name作用/课堂代码/1. FolderTree/.prettierrc.json b/20. 组件name作用/课堂代码/1. FolderTree/.prettierrc.json similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/.prettierrc.json rename to 20. 组件name作用/课堂代码/1. FolderTree/.prettierrc.json diff --git a/19. 组件name作用/课堂代码/1. FolderTree/.vscode/extensions.json b/20. 组件name作用/课堂代码/1. FolderTree/.vscode/extensions.json similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/.vscode/extensions.json rename to 20. 组件name作用/课堂代码/1. FolderTree/.vscode/extensions.json diff --git a/19. 组件name作用/课堂代码/1. FolderTree/README.md b/20. 组件name作用/课堂代码/1. FolderTree/README.md similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/README.md rename to 20. 组件name作用/课堂代码/1. FolderTree/README.md diff --git a/19. 组件name作用/课堂代码/1. FolderTree/index.html b/20. 组件name作用/课堂代码/1. FolderTree/index.html similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/index.html rename to 20. 组件name作用/课堂代码/1. FolderTree/index.html diff --git a/19. 组件name作用/课堂代码/1. FolderTree/jsconfig.json b/20. 组件name作用/课堂代码/1. FolderTree/jsconfig.json similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/jsconfig.json rename to 20. 组件name作用/课堂代码/1. FolderTree/jsconfig.json diff --git a/19. 组件name作用/课堂代码/1. FolderTree/package-lock.json b/20. 组件name作用/课堂代码/1. FolderTree/package-lock.json similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/package-lock.json rename to 20. 组件name作用/课堂代码/1. FolderTree/package-lock.json diff --git a/19. 组件name作用/课堂代码/1. FolderTree/package.json b/20. 组件name作用/课堂代码/1. FolderTree/package.json similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/package.json rename to 20. 组件name作用/课堂代码/1. FolderTree/package.json diff --git a/19. 组件name作用/课堂代码/1. FolderTree/public/favicon.ico b/20. 组件name作用/课堂代码/1. FolderTree/public/favicon.ico similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/public/favicon.ico rename to 20. 组件name作用/课堂代码/1. FolderTree/public/favicon.ico diff --git a/19. 组件name作用/课堂代码/1. FolderTree/src/App.vue b/20. 组件name作用/课堂代码/1. FolderTree/src/App.vue similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/src/App.vue rename to 20. 组件name作用/课堂代码/1. FolderTree/src/App.vue diff --git a/19. 组件name作用/课堂代码/1. FolderTree/src/assets/base.css b/20. 组件name作用/课堂代码/1. FolderTree/src/assets/base.css similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/src/assets/base.css rename to 20. 组件name作用/课堂代码/1. FolderTree/src/assets/base.css diff --git a/19. 组件name作用/课堂代码/1. FolderTree/src/assets/logo.svg b/20. 组件name作用/课堂代码/1. FolderTree/src/assets/logo.svg similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/src/assets/logo.svg rename to 20. 组件name作用/课堂代码/1. FolderTree/src/assets/logo.svg diff --git a/19. 组件name作用/课堂代码/1. FolderTree/src/assets/main.css b/20. 组件name作用/课堂代码/1. FolderTree/src/assets/main.css similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/src/assets/main.css rename to 20. 组件name作用/课堂代码/1. FolderTree/src/assets/main.css diff --git a/19. 组件name作用/课堂代码/1. FolderTree/src/components/FolderTree.vue b/20. 组件name作用/课堂代码/1. FolderTree/src/components/FolderTree.vue similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/src/components/FolderTree.vue rename to 20. 组件name作用/课堂代码/1. FolderTree/src/components/FolderTree.vue diff --git a/19. 组件name作用/课堂代码/1. FolderTree/src/main.js b/20. 组件name作用/课堂代码/1. FolderTree/src/main.js similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/src/main.js rename to 20. 组件name作用/课堂代码/1. FolderTree/src/main.js diff --git a/19. 组件name作用/课堂代码/1. FolderTree/vite.config.js b/20. 组件name作用/课堂代码/1. FolderTree/vite.config.js similarity index 100% rename from 19. 组件name作用/课堂代码/1. FolderTree/vite.config.js rename to 20. 组件name作用/课堂代码/1. FolderTree/vite.config.js diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/.eslintrc.cjs b/20. 组件name作用/课堂代码/2. KeepAlive/.eslintrc.cjs similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/.eslintrc.cjs rename to 20. 组件name作用/课堂代码/2. KeepAlive/.eslintrc.cjs diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/.gitignore b/20. 组件name作用/课堂代码/2. KeepAlive/.gitignore similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/.gitignore rename to 20. 组件name作用/课堂代码/2. KeepAlive/.gitignore diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/.prettierrc.json b/20. 组件name作用/课堂代码/2. KeepAlive/.prettierrc.json similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/.prettierrc.json rename to 20. 组件name作用/课堂代码/2. KeepAlive/.prettierrc.json diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/.vscode/extensions.json b/20. 组件name作用/课堂代码/2. KeepAlive/.vscode/extensions.json similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/.vscode/extensions.json rename to 20. 组件name作用/课堂代码/2. KeepAlive/.vscode/extensions.json diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/README.md b/20. 组件name作用/课堂代码/2. KeepAlive/README.md similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/README.md rename to 20. 组件name作用/课堂代码/2. KeepAlive/README.md diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/index.html b/20. 组件name作用/课堂代码/2. KeepAlive/index.html similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/index.html rename to 20. 组件name作用/课堂代码/2. KeepAlive/index.html diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/jsconfig.json b/20. 组件name作用/课堂代码/2. KeepAlive/jsconfig.json similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/jsconfig.json rename to 20. 组件name作用/课堂代码/2. KeepAlive/jsconfig.json diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/package-lock.json b/20. 组件name作用/课堂代码/2. KeepAlive/package-lock.json similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/package-lock.json rename to 20. 组件name作用/课堂代码/2. KeepAlive/package-lock.json diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/package.json b/20. 组件name作用/课堂代码/2. KeepAlive/package.json similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/package.json rename to 20. 组件name作用/课堂代码/2. KeepAlive/package.json diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/public/favicon.ico b/20. 组件name作用/课堂代码/2. KeepAlive/public/favicon.ico similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/public/favicon.ico rename to 20. 组件name作用/课堂代码/2. KeepAlive/public/favicon.ico diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/App.vue b/20. 组件name作用/课堂代码/2. KeepAlive/src/App.vue similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/App.vue rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/App.vue diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/assets/base.css b/20. 组件name作用/课堂代码/2. KeepAlive/src/assets/base.css similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/assets/base.css rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/assets/base.css diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/assets/jinzhu.jpeg b/20. 组件name作用/课堂代码/2. KeepAlive/src/assets/jinzhu.jpeg similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/assets/jinzhu.jpeg rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/assets/jinzhu.jpeg diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/assets/landscape.jpeg b/20. 组件name作用/课堂代码/2. KeepAlive/src/assets/landscape.jpeg similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/assets/landscape.jpeg rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/assets/landscape.jpeg diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/assets/logo.svg b/20. 组件name作用/课堂代码/2. KeepAlive/src/assets/logo.svg similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/assets/logo.svg rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/assets/logo.svg diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/assets/main.css b/20. 组件name作用/课堂代码/2. KeepAlive/src/assets/main.css similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/assets/main.css rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/assets/main.css diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/assets/yinshi.jpg b/20. 组件name作用/课堂代码/2. KeepAlive/src/assets/yinshi.jpg similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/assets/yinshi.jpg rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/assets/yinshi.jpg diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/components/CheckboxList.vue b/20. 组件name作用/课堂代码/2. KeepAlive/src/components/CheckboxList.vue similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/components/CheckboxList.vue rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/components/CheckboxList.vue diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/components/Counter.vue b/20. 组件name作用/课堂代码/2. KeepAlive/src/components/Counter.vue similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/components/Counter.vue rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/components/Counter.vue diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/components/TextInput.vue b/20. 组件name作用/课堂代码/2. KeepAlive/src/components/TextInput.vue similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/components/TextInput.vue rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/components/TextInput.vue diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/components/Timer.vue b/20. 组件name作用/课堂代码/2. KeepAlive/src/components/Timer.vue similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/components/Timer.vue rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/components/Timer.vue diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/main.js b/20. 组件name作用/课堂代码/2. KeepAlive/src/main.js similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/main.js rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/main.js diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/src/router/index.js b/20. 组件name作用/课堂代码/2. KeepAlive/src/router/index.js similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/src/router/index.js rename to 20. 组件name作用/课堂代码/2. KeepAlive/src/router/index.js diff --git a/19. 组件name作用/课堂代码/2. KeepAlive/vite.config.js b/20. 组件name作用/课堂代码/2. KeepAlive/vite.config.js similarity index 100% rename from 19. 组件name作用/课堂代码/2. KeepAlive/vite.config.js rename to 20. 组件name作用/课堂代码/2. KeepAlive/vite.config.js diff --git a/20. Vue项目性能优化/课件资料/Vue项目性能优化.md b/21. Vue项目性能优化/课件资料/Vue项目性能优化.md similarity index 100% rename from 20. Vue项目性能优化/课件资料/Vue项目性能优化.md rename to 21. Vue项目性能优化/课件资料/Vue项目性能优化.md diff --git a/21. 路由传参方式/课件资料/路由传参方式.md b/22. 路由传参方式/课件资料/路由传参方式.md similarity index 100% rename from 21. 路由传参方式/课件资料/路由传参方式.md rename to 22. 路由传参方式/课件资料/路由传参方式.md diff --git a/21. 路由传参方式/课堂代码/demo/.eslintrc.cjs b/22. 路由传参方式/课堂代码/demo/.eslintrc.cjs similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/.eslintrc.cjs rename to 22. 路由传参方式/课堂代码/demo/.eslintrc.cjs diff --git a/21. 路由传参方式/课堂代码/demo/.gitignore b/22. 路由传参方式/课堂代码/demo/.gitignore similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/.gitignore rename to 22. 路由传参方式/课堂代码/demo/.gitignore diff --git a/21. 路由传参方式/课堂代码/demo/.prettierrc.json b/22. 路由传参方式/课堂代码/demo/.prettierrc.json similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/.prettierrc.json rename to 22. 路由传参方式/课堂代码/demo/.prettierrc.json diff --git a/21. 路由传参方式/课堂代码/demo/.vscode/extensions.json b/22. 路由传参方式/课堂代码/demo/.vscode/extensions.json similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/.vscode/extensions.json rename to 22. 路由传参方式/课堂代码/demo/.vscode/extensions.json diff --git a/21. 路由传参方式/课堂代码/demo/README.md b/22. 路由传参方式/课堂代码/demo/README.md similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/README.md rename to 22. 路由传参方式/课堂代码/demo/README.md diff --git a/21. 路由传参方式/课堂代码/demo/index.html b/22. 路由传参方式/课堂代码/demo/index.html similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/index.html rename to 22. 路由传参方式/课堂代码/demo/index.html diff --git a/21. 路由传参方式/课堂代码/demo/jsconfig.json b/22. 路由传参方式/课堂代码/demo/jsconfig.json similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/jsconfig.json rename to 22. 路由传参方式/课堂代码/demo/jsconfig.json diff --git a/21. 路由传参方式/课堂代码/demo/package-lock.json b/22. 路由传参方式/课堂代码/demo/package-lock.json similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/package-lock.json rename to 22. 路由传参方式/课堂代码/demo/package-lock.json diff --git a/21. 路由传参方式/课堂代码/demo/package.json b/22. 路由传参方式/课堂代码/demo/package.json similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/package.json rename to 22. 路由传参方式/课堂代码/demo/package.json diff --git a/21. 路由传参方式/课堂代码/demo/public/favicon.ico b/22. 路由传参方式/课堂代码/demo/public/favicon.ico similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/public/favicon.ico rename to 22. 路由传参方式/课堂代码/demo/public/favicon.ico diff --git a/21. 路由传参方式/课堂代码/demo/src/App.vue b/22. 路由传参方式/课堂代码/demo/src/App.vue similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/App.vue rename to 22. 路由传参方式/课堂代码/demo/src/App.vue diff --git a/21. 路由传参方式/课堂代码/demo/src/assets/base.css b/22. 路由传参方式/课堂代码/demo/src/assets/base.css similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/assets/base.css rename to 22. 路由传参方式/课堂代码/demo/src/assets/base.css diff --git a/21. 路由传参方式/课堂代码/demo/src/assets/logo.svg b/22. 路由传参方式/课堂代码/demo/src/assets/logo.svg similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/assets/logo.svg rename to 22. 路由传参方式/课堂代码/demo/src/assets/logo.svg diff --git a/21. 路由传参方式/课堂代码/demo/src/assets/main.css b/22. 路由传参方式/课堂代码/demo/src/assets/main.css similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/assets/main.css rename to 22. 路由传参方式/课堂代码/demo/src/assets/main.css diff --git a/21. 路由传参方式/课堂代码/demo/src/assets/styles.css b/22. 路由传参方式/课堂代码/demo/src/assets/styles.css similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/assets/styles.css rename to 22. 路由传参方式/课堂代码/demo/src/assets/styles.css diff --git a/21. 路由传参方式/课堂代码/demo/src/components/Book.vue b/22. 路由传参方式/课堂代码/demo/src/components/Book.vue similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/components/Book.vue rename to 22. 路由传参方式/课堂代码/demo/src/components/Book.vue diff --git a/21. 路由传参方式/课堂代码/demo/src/components/Home.vue b/22. 路由传参方式/课堂代码/demo/src/components/Home.vue similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/components/Home.vue rename to 22. 路由传参方式/课堂代码/demo/src/components/Home.vue diff --git a/21. 路由传参方式/课堂代码/demo/src/components/Profile.vue b/22. 路由传参方式/课堂代码/demo/src/components/Profile.vue similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/components/Profile.vue rename to 22. 路由传参方式/课堂代码/demo/src/components/Profile.vue diff --git a/21. 路由传参方式/课堂代码/demo/src/components/Stu.vue b/22. 路由传参方式/课堂代码/demo/src/components/Stu.vue similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/components/Stu.vue rename to 22. 路由传参方式/课堂代码/demo/src/components/Stu.vue diff --git a/21. 路由传参方式/课堂代码/demo/src/components/User.vue b/22. 路由传参方式/课堂代码/demo/src/components/User.vue similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/components/User.vue rename to 22. 路由传参方式/课堂代码/demo/src/components/User.vue diff --git a/21. 路由传参方式/课堂代码/demo/src/main.js b/22. 路由传参方式/课堂代码/demo/src/main.js similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/main.js rename to 22. 路由传参方式/课堂代码/demo/src/main.js diff --git a/21. 路由传参方式/课堂代码/demo/src/router/index.js b/22. 路由传参方式/课堂代码/demo/src/router/index.js similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/router/index.js rename to 22. 路由传参方式/课堂代码/demo/src/router/index.js diff --git a/21. 路由传参方式/课堂代码/demo/src/views/AboutView.vue b/22. 路由传参方式/课堂代码/demo/src/views/AboutView.vue similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/views/AboutView.vue rename to 22. 路由传参方式/课堂代码/demo/src/views/AboutView.vue diff --git a/21. 路由传参方式/课堂代码/demo/src/views/HomeView.vue b/22. 路由传参方式/课堂代码/demo/src/views/HomeView.vue similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/src/views/HomeView.vue rename to 22. 路由传参方式/课堂代码/demo/src/views/HomeView.vue diff --git a/21. 路由传参方式/课堂代码/demo/vite.config.js b/22. 路由传参方式/课堂代码/demo/vite.config.js similarity index 100% rename from 21. 路由传参方式/课堂代码/demo/vite.config.js rename to 22. 路由传参方式/课堂代码/demo/vite.config.js