Vue设计与实现
1.框架设计
1.命令式与声明式的权衡
| 命令式 | 告诉程序怎么做(一步步操作) |
|---|---|
| 声明式 | 告诉程序做什么(描述状态或结构) |
vue的内部实现是命令式的,暴露给用户的是声明式的。
2.性能与可维护性的权衡
| 命令式 | 声明式 |
|---|---|
| 性能高 | 性能低 |
| 可维护性差 | 可维护性高 |
vue使用虚拟DOM来在保持可维护性的同时让性能损失最小化
3.innerHTML、虚拟DOM、原生JavaScript在更新页面时的差异
| innerHTML | 虚拟DOM | 原生JavaScript |
|---|---|---|
| 心智负担中等 | 心智负担小 | 心智负担大 |
| 性能差 | 可维护性强 | 可维护性差 |
| 性能不错 | 性能高 |
4.运行时和编译时
在Vue中编译时和运行时具体表现
**运行时:**通过Render函数将传入的虚拟DOM渲染成DOM元素
编译时:将HTML标签编译成树型结构的对象
添加编译过程,可以分析用户的输入部分那些是可变的,哪些是不变的,有助于做进一步的优化。添加运行过程,可以增加代码的灵活性。
2.框架设计的核心要素
1.错误信息打印
在Vue中使用warn函数来打印警告信息。
在Chrome打开Console------>Enable custom formatters,输出更加直观的打印信息
2.框架体积的控制
通过DEV常量来区分开发环境
使用Tree-Shaking 来消除永远不会执行的代码,在Vue.js源码中大量使用了PURE注释来标注无副作用。
3.Vue.js3的设计思路
1.声明式地描述UI
vue3支持模板描述UI,还支持虚拟DOM描述UI,h函数返回一个虚拟DOM对象,
2.渲染器
渲染器用于将虚拟DOM渲染为真实DOM
3.组件的本质
组件是一组DOM元素的封装,它可以是一个返回虚拟DOM的函数,也可以是一个对象,但这个对象下必须要有一个函数来产出组件要渲染的虚拟DOM。
4.模板的工作原理
编译器会把模板内容编译成渲染函数并添加到
!IMPORTANT
无论是使用模板还是直接手写渲染函数,对于一个组件来说,他要渲染的内容最终都是通过渲染函数生成的,然后渲染器再把渲染函数返回的虚拟DOM渲染为真实DOM。
模板(编译器)------>渲染函数/h()------>虚拟DOM(渲染器)------>真实DOM
4.响应系统的实现
1.响应式数据和副作用函数
副作用函数指的是会产生副作用的函数
javascript
function effect() {
document.body.innerText = 'hello vue3'
}
当对象的值变化后,副作用函数自动重新执行,那对象就是响应式数据
2.响应式系统的实现
拦截一个对象的读取和设置
当读取操作发生时,将副作用函数收集到桶中;
当设置操作发生时,从桶中取出副作用函数执行;
桶结构的实现:WeakMap、Map、Set WeakMap由target------>Map构成 Map由key------Set构成
WeakMap的键是原始对象target,WeakMap的值是一个Map实例,而Map的键是原始对象target的key,Map的值是一个由副作用函数组成的Set。
具体实现:
javascript
// 用一个全局变量存储被注册的副作用函数
let activeEffect
// effect用于注册副作用函数
function effect(fn) {
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = fn
// 执行副作用函数
fn()
}
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数activeEffect添加到存储副作用函数的桶中
track(target, key)
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
target[key] = newVal
// 把副作用函数取出并执行
trigger(target, key)
}
})
// 在get拦截函数内调用track函数追踪变化
function track(traget, key){
if(!affectiveEffect) return
let depsMap = bucket.get(target)
if(!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps depsMap.get(key)
if(!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
// 在set拦截函数内调用trigger函数触发变化
function trigger(target, key) {
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
3实现细节
-
分支切换与cleanup 方案:给每个副作用函数,收集依赖集合,执行前从每个依赖集合中移除。
-
嵌套的effect与effect栈
方案:使用栈来存储嵌套的副作用函数,让activeEffect指向栈顶元素,副作用函数执行前入栈,执行后出栈
-
避免无限循环
方案:如果trigger触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
-
调度执行
Vue内部实现了一个更加完善的调度器,在连续多次修改响应式数据但只会触发一次更新 简化思路
javascriptconst jobQueue = new Set<Function>(); let isFlushing = false; function queueJob(job: Function) { jobQueue.add(job); if (!isFlushing) { isFlushing = true; Promise.resolve().then(() => { jobQueue.forEach(fn => fn()); jobQueue.clear(); isFlushing = false; }); } }
4.computed函数的实现
javascript
function computed(gettet) {
// value 用来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true则意味着脏,需要计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(effect, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
dirty = true
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发响应
trigger(obj, 'value')
}
})
const obj = {
// 当读取value时才执行effectFn
get value() {
if(dirty) {
value = effectFn()
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
5.watch的实现原理
watch本质就是观测一个响应式数据或者getter函数,当数据发生变化时通知并执行相应的回调函数
javascript
function watch(source, cb) {
effect(
// 调用traverse递归的读取
() => traverse(source)
, {
scheduler() {
// 当有数据变化时,调用回调函数cb
cb()
}
})
}
function traverse(value, seen = new Set()) {
// 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
if(typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用引起的死循环
seen.add(value)
// 如果是一个对象,使用for...in 读取对象的每一个值,并递归处理
for(const k in value) {
traverse(values[k], seen)
}
return value
}
竞态问题:
javascript
watch(obj, async (newValue, oldValue, onInvalidate) => {
// 定义一个标志,代表当前副作用函数是否过期,默认为false,代表没有过期
let expired = false
// 调用onInvalidate()函数注册一个过期回调
onInvalidate(() => {
// 当过期时,将expired设置为true
expired = true
})
})
5.非原始值的响应式方案
1.Proxy和Reflect
javascript
const p = new Proxy(obj, {
// 拦截读取操作,接收第三个参数receiver
get(target, key, receiver) {
track(target, key)
// 使用Reflect.get返回读取到的属性值,receiver表示读取属性的对象
return Reflect.get(target, key, receiver)
}
})
2.如何代理Object/Array/集合类型
javascript
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// 代理对象可以通过raw属性访问原始数据
if(key === 'raw') {
return target
}
track(target, key)
// 使用Reflect.get返回读取到的属性值,receiver表示读取属性的对象
return Reflect.get(target, key, receiver)
}
})
}
Vue 3 实现响应式系统的核心就是利用 Proxy 来拦截对象的各种操作,而对于不同的数据结构(Object、Array、Set、Map),Vue 都是通过**拦截它们的"原始操作"**来实现响应追踪和更新。
🔧 各类型拦截核心原理:
1. 普通对象(Object)
-
拦截属性的 读写 (
get/set):jsconst proxy = new Proxy(target, { get(target, key, receiver) { track(target, key) // 收集依赖 return Reflect.get(target, key, receiver) }, set(target, key, val, receiver) { const result = Reflect.set(target, key, val, receiver) trigger(target, key) // 触发更新 return result } })
2. 数组(Array)
-
在拦截
get时,判断是否访问了 数组方法 (如push、splice等),并为这些方法单独处理。 -
例如:
jsget(target, key, receiver) { const res = Reflect.get(target, key, receiver) if (typeof res === 'function' && ['push', 'splice', ...].includes(key)) { // 包一层:先 track,再调用原始方法,再 trigger return function (...args) { const result = Array.prototype[key].apply(this, args) trigger(target, 'length') // 通知更新 return result } } return res }
3. Set / Map
- 拦截
get,并对.add()、.set()、.delete()、.has()等原始方法进行包装。
Vue 的做法是:
js
const proxy = new Proxy(originalSetOrMap, {
get(target, key, receiver) {
if (key === 'add') {
return function (value) {
const result = target.add(value)
trigger(target, 'add', value)
return result
}
}
// 类似处理:set、delete、has、clear 等
return Reflect.get(target, key, receiver)
}
})
核心在于:拦截方法调用 → 包装方法 → 在调用原方法前后进行 track/trigger。
💡 总结一句话:
Vue 响应式的核心就是通过
Proxy拦截原始数据结构的"原生行为",在执行它们之前或之后注入依赖收集和派发更新逻辑,从而实现响应式追踪。
如果你想手写一个支持 Set 的响应式系统,我可以帮你从 get 拦截扩展到 add() 的完整代码,要试试吗?
6.原始值的响应式方案
1.ref的实现
javascript
function ref(val) {
const wrapper = {
value: val
}
// 使用Objecy.defineProperty在wrapper对象上定义一个不可枚举的属性__v_isRef,并且值为true
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
return reactive(wrapper)
}
2.响应丢失问题
当使用展开运算符将代理对象赋值给新对象时,会导致丢失响应式。
javascript
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
},
set value(val) {
return obj[key] = val
}
}
// 定义__v_isRef属性
Object.defineProperty(wrapper, '__v_isRef', {
value: true
})
return wrapper
}
// 使用方式
const newObj = {
foo: toRef(obj, 'foo'),
bar: toRef(obj, 'bar')
}
// 批量转换
function toRefs(obj) {
const ret = {}
for(const key in obj) {
// 逐个调用toRef完成转换
ret[key] = toRef(obj, key)
}
return ret
}
// 使用方式
const newObj = {...toRefs(obj)}
3.自动脱ref
| 使用场景 | 是否自动脱 .value |
示例 | 说明 |
|---|---|---|---|
| 模板中(template) | ✅ 是 | {{ count }} |
会自动取 .value |
reactive 中的属性访问 |
✅ 是 | state.count |
如果属性是 ref,会自动返回 .value |
| 直接访问 ref 对象 | ❌ 否 | count.value |
不自动脱,需要手动 .value |
解构 ref 时(如 const { x } = objRef) |
❌ 否 | x.value |
解构后失去响应性,需要手动访问 |
toRefs / toRef 的结果访问 |
❌ 否 | stateRef.a.value |
返回的本身就是 ref,不会自动脱 |
setup() 返回值传给模板 |
✅ 是 | <template>{{ msg }}</template> |
会自动解包 |
| computed 的结果在模板中使用 | ✅ 是 | {{ doubleCount }} |
自动脱 .value |
| computed 的结果在 JS 中使用 | ❌ 否 | doubleCount.value |
在 JS 中仍需 .value |
7.渲染器
渲染器:把虚拟DOM渲染为特定平台上的真实元素。在游览器平台上,渲染器会把虚拟DOM渲染为真实DOM元素。
挂载:渲染器把虚拟DOM节点渲染为真实DOM节点的过程。用mount来表达。在mounted钩子中可以访问真实DOM元素。
容器元素:真实DOM元素的挂载位置。
javascript
function createRenderer() {
function render(vnode, container) {
if(vnode) {
// 新vnode存在,将其与旧vnode一起传递给patch函数,进行打补丁
patch(container._vnode, vnode, container)
}
else {
if(container._vnode) {
// 旧vnode存在,说明是卸载操作
container.innerHTML = ""
}
}
// 把vnode存储到container._vnode下,及后续渲染中的旧vnode
container._vnode = vnode
}
return {
render
}
}
自定义渲染器:
javascript
// 在创建renderer时传入配置项
const renderer = createRenderer({
// 用于创建元素
createElement(tag) {
return document.createElement(tag)
},
// 用于设置元素的文本节点
setElementText(el, text) {
el.textContent = text
},
// 用于在给定的parent下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor)
}
})
8.挂载和更新
挂载和更新是渲染器的核心功能。
1.挂载子节点和和元素的属性
javascript
function mountElement(vnode, container) {
const el = createElement(vnode.type)
if(typeof vnode.children === 'string') {
setElementText(el, vnode.children)
}
else if (Array.isArray(vnode.children)) {
// 如果children是一个数组,则遍历每一个子节点,并调用patch函数挂载它们
vnode.children.forEach(child => {
patch(null, child, el)
})
}
insert(el, container)
}
2.HTML Attribute与DOM Properties
HTML Attributes 是写在 HTML 标签上的属性,而 DOM Properties 是 JavaScript 中 DOM 对象的属性。
| 比较项 | HTML Attribute | DOM Property |
|---|---|---|
| 定义 | 写在 HTML 标签中的静态属性 | DOM 元素在 JS 中的动态属性 |
| 使用方式 | <input value="text"> |
input.value = "text" |
| 生命周期 | 页面加载时被浏览器解析 | 页面加载后在 JS 中操作 |
| 可否变化 | 通常静态(除非通过 JS 设置) | 动态可变 |
| 彼此是否同步 | 初始时同步,一旦修改 DOM 属性就不同步了 | 改变属性不会影响 attribute,反之亦然 |
| 示例 | class, value, id, disabled |
className, value, id, disabled |
HTML Attribute的作用是设置与之对应的DOM Properties的初始值
3.元素属性的挂载
javascript
const renderer = createRenderer({
// 省略其他项
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);
}
},
});
4.卸载操作
根据vnode对象获取与其相关联的真实DOM元素,然后使用原生DOM操作的方法将该DOM元素移除
javascript
function mountElement(vnode, container) {
// 让vnode.el引用真实DOM元素
const el = vnode.el = document.createElement(vnode.type);
if(typeof vnode.children === "string") {
setElementText(el, vnode.children);
} else if(Array.isArray(vnode.children)) {
// 递归渲染
vnode.children.forEach(child => {
patch(null, child, el);
});
}
if(vnode.props) {
for(const key in vnode.props) {
patchProps(el, key, null, vnode.props[key]);
}
}
insert(el, container);
}
function render(vnode, container) {
if(vnode) {
patch(container._vnode, vnode, container)
} else {
if(container._vnode) {
// 根据vnode获取要卸载的真实dom
const el = container._vnode.el
// 获取el的父节点
const parentElm = el.parentNode
// 调用父节点的removeChild方法移除el
parentElm.removeChild(el)
}
}
container._vnode = vnode
}
5.新旧vnode类型判断
javascript
function patch(n1, n2, container) {
// 如果n1存在,则对比n1和n2的类型
if(n1 && n1.type !== n2.type) {
如果新旧vnode的类型不同,则直接将旧vnode卸载
unmount(n1)
n1 = null
}
// 代码运行到这里,证明n1和n2所描述的内容相同
const { type } = n2
// 如果n2.type的值是字符串类型,则它描述的是普通标签元素
if(typeof type === 'string') {
if(!n1) {
mountElement(n2, container)
} else {
// 更新
patchElement(n1, n2)
}
} else if(typeof type === 'object') {
// 如果n2.type的值的类型是对象,则它描述的是组件
} else if (type === 'xxx') {
// 处理其他类型的vnode
}
}
6.事件的处理
事件可以视作一种特殊的属性
javascript
const vnode = {
type: 'p',
props: {
onClick: () => {
alert('clicked')
}
},
children: 'text'
}
patchProps(el, key, prevVal, nextVal) {
if(/^on/.test(key)) {
// 获取为该元素伪造的事件处理函数 invoker
let invoker = el._vel;
const name = key.slice(2).toLowerCase();
if(nextVal) {
if(!invoker) {
// 如果没有invoker,则将一个伪造的invoker缓存到el._vel上
// vel 是 vue event invoker 的缩写
invoker = el._vel = (e) => {
// 当伪造的事件处理函数执行时,会执行真正的事件处理函数
invoker.value(e);
}
// 将真正的事件处理函数赋值给invoker.value
invoker.value = nextVal;
// 绑定invoker作为事件处理函数
el.addEventListener(name, invoker);
} else {
// 如果已经存在invoker,只需要更新invoker.value即可
invoker.value = nextVal;
}
} else if(invoker) {
// 新的事件绑定函数不存在,且之前的事件绑定函数存在,则移除事件绑定
el.removeEventListener(name, invoker);
}
}
}
7.更新子节点
javascript
function patchChildren(n1, n2, container) {
// 判断新子节点的类型是否为文本
if (typeof n2 === "string") {
// 旧子节点的类型有三种可能:没有子节点、文本子节点以及一组子节点
// 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况什么都不需要做
if(Array.isArray(n1.children)) {
n1.children.forEach(child => {
unmount(child);
});
}
// 最后将新的文本节点内容设置给容器元素
setElementText(container, n2.children);
} else if(Array.isArray(n2.children)) {
// 新子节点为一组子节点时,需要进行精细化比较
// 判断旧子节点是否为一组子节点
if(Array.isArray(n1.children)) {
// 核心diff算法
} else {
// 此时旧子节点要么是文本子节点,要么是没有子节点
// 无论哪种情况,都需要将旧子节点卸载
setElementText(container, "");
n2.children.forEach(child => {
patch(null, child, container);
});
}
} else {
// 新子节点既不是文本子节点,也不是一组子节点
// 旧子节点是一组子节点,只需逐个卸载即可
if(Array.isArray(n1.children)) {
n1.children.forEach(child => {
unmount(child);
});
}
else if(typeof n1 === "string") {
// 旧子节点是文本子节点,直接设置容器元素的文本内容为空字符串即可
setElementText(container, "");
}
// 如果也没有旧子节点,那么什么都不需要做
}
}
8.文本节点和注释节点
javascript
// 文本节点的type标识
const Text = Symbol()
const newVnode = {
type: "Text",
children: '我是文本内容'
}
// 注释节点的type标识
const Comment = Symbol()
const newVnode2 = {
type: "Comment",
children: '我是注释内容'
}
9.Fragment
vue3使用Fragment来实现多根节点模板。Fragment本身不会下渲染任何DOM元素,只需要渲染一个Fragment的所有子节点即可
javascript
const Fragment = Symbol()
const vnode = {
type: Fragment,
children: [
{ type: 'li', children: 'text 1' },
{ type: 'li', children: 'text 2' },
{ type: 'li', children: 'text 3' }
]
}
9.Diff算法
当新旧vnode的子节点都是一组节点时,为了以最小的性能开销完成更新操作,需要比较两组子节点时,用于比较的算法就叫做Diff算法。
1.简单Diff算法
核心逻辑:拿新的一组子节点中的节点去旧的一组子节点中寻找可复用的节点。如果找到了,则记录该节点的位置索引。我们把这个位置索引称为最大索引。在整个更新过程中,如果一个节点的索引值小于最大索引,则说明该节点对应的真实DOM元素需要移动。
javascript
function patchChildren(n1, n2, container) {
if (typeof n2 === "string") {
n2 = createVNode(n2);
}
else if (Array.isArray(n2)) {
const oldChildren = n1.children;
cibst newchildren = n2.children;
let lastIndex = 0;
for (let i = 0; i < newchildren.length; i++) {
const newVNode = newchildren[i];
let j = 0
for (; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j];
if(newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container)
}
if( j < lastIndex) {
// 代码运行到这里,说明newVNode对应的真实DOM需要移动
// 先获取newVNode的前一个vnode,即prevVNode
const prevVNode = newchildren[i - 1];
// 如果prevVNode不存在,则说明当前newVNode是第一个节点,它不需要移动
if(prevVNode) {
// 由于我们需要将newVNode对应的真实DOM移动到prevVNode对应的真实DOM的后面
// 所以我们需要获取prevVNode对应的真实DOM的下一个节点,并将其作为锚点
const anchor = prevVNode.el.nextSibling;
// 将newVNode对应的真实DOM插入到prevVNode对应的真实DOM的后面
insert(newVNode.el, container, anchor)
}
}
else {
lastIndex = j;
}
break
}
}
}
}
2.双端Diff算法
双端Diff算法指的是,在新旧两组子节点的四个端点进行比较,并试图找到可复用的节点。相比简单Diff算法,双端Diff算法的优势在于,对于同样的更新场景,执行的DOM移动操作次数更少。
javascript
function patchKeyedChildren(n1, n2, container) {
const oldChildren = n1.children;
const newChildren = n2.children;
// 四个索引值
let oldStartIndex = 0;
let newStartIndex = 0;
let oldEndIndex = oldChildren.length - 1;
let newEndIndex = newChildren.length - 1;
// 四个索引指向的vnode节点
let oldStartVNode = oldChildren[0];
let newStartVNode = newChildren[0];
let oldEndVNode = oldChildren[oldEndIndex];
let newEndVNode = newChildren[newEndIndex];
if(oldStartVNode.key === newStartVNode.key) {
// 第一步:头头比对
patch(oldStartVNode, newStartVNode, container)
oldStartIndex++;
newStartIndex++;
}
else if(oldEndVNode.key === newEndVNode.key) {
// 第二步:尾尾比对
patch(oldEndVNode, newEndVNode, container)
oldEndIndex--;
newEndIndex--;
}
else if(oldStartVNode.key === newEndVNode.key) {
// 第三步:头尾比对
patch(oldStartVNode, newEndVNode, container)
oldStartIndex++;
newEndIndex--;
}
else if(oldEndVNode.key === newStartVNode.key) {
// 第四步:尾头比对
patch(oldEndVNode, newStartVNode, container)
oldEndIndex--;
newStartIndex++;
}
}
3.快速Diff算法
快速Diff算法借鉴了文本Diff中的预处理思路,先处理新旧两组子节点中相同的前置节点和相同的后置节点。当前置节点和后置节点全部处理完毕后,如果无法简单地通过挂载新节点或者卸载以及不存在的节点来完成更新,则需要根据节点的索引关系,构造出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。
javascript
type VNode = {
key: string | number;
el?: any;
};
function isSameVNode(n1: VNode, n2: VNode) {
return n1.key === n2.key;
}
function patch(n1: VNode, n2: VNode) {
console.log(`patch ${n1.key} -> ${n2.key}`);
}
function mount(n: VNode, pos: number) {
console.log(`mount ${n.key} at position ${pos}`);
}
function move(n: VNode, pos: number) {
console.log(`move ${n.key} to position ${pos}`);
}
function getLIS(arr: number[]): number[] {
const p = arr.slice();
const result: number[] = [];
let u: number, v: number;
for (let i = 0; i < arr.length; i++) {
const n = arr[i];
if (n === -1) continue;
if (result.length === 0 || arr[result[result.length - 1]] < n) {
p[i] = result.length > 0 ? result[result.length - 1] : -1;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
const c = (u + v) >> 1;
if (arr[result[c]] < n) u = c + 1;
else v = c;
}
if (n < arr[result[u]]) {
if (u > 0) p[i] = result[u - 1];
result[u] = i;
}
}
let len = result.length;
let i = result[len - 1];
const lis = Array(len);
while (len-- > 0) {
lis[len] = i;
i = p[i];
}
return lis;
}
function quickDiff(oldChildren: VNode[], newChildren: VNode[]) {
const oldKeyToIndexMap = new Map<string | number, number>();
oldChildren.forEach((node, idx) => oldKeyToIndexMap.set(node.key, idx));
const newIndexToOldIndex = new Array(newChildren.length).fill(-1);
for (let i = 0; i < newChildren.length; i++) {
const newNode = newChildren[i];
const oldIdx = oldKeyToIndexMap.get(newNode.key);
if (oldIdx != null) {
newIndexToOldIndex[i] = oldIdx;
patch(oldChildren[oldIdx], newNode);
} else {
mount(newNode, i);
}
}
// 求最长递增子序列(表示不需要移动的节点)
const lis = getLIS(newIndexToOldIndex);
let lisIdx = lis.length - 1;
// 从后向前检查:不在 LIS 中的需要移动
for (let i = newChildren.length - 1; i >= 0; i--) {
if (newIndexToOldIndex[i] === -1) continue; // 新增的已经挂载过
if (i !== lis[lisIdx]) {
move(newChildren[i], i);
} else {
lisIdx--;
}
}
}
10.组件化
一个组件是一个特殊类型的虚拟DOM节点,type属性存储组件的选项对象
javascript
const vnode = {
type: MyComponent
}
// 选项对象
const MyComponent = {
// 组件名称,可选
name: 'MyComponent',
// 组件的渲染函数,其返回值必须为虚拟DOM
render() {
// 返回虚拟DOM
return {
type: 'div',
children: '我是文本内容'
}
}
}
1.渲染器渲染组件
javascript
function mountComponent(vnode, container, anchor) {
// 通过vnode获取组件的选项对象,即vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数
const { render } = componentOptions
// 执行渲染函数,获取组件要渲染的内容,即render函数返回的虚拟DOM
const subTree = render()
// 最后调用patch函数来挂载组件所描述的内容,即subTree
patch(null, subTree, container, anchor)
}
2.组件的选项对象
javascript
// 选项对象
const MyComponent = {
// 组件名称,可选
name: 'MyComponent',
// 用data函数来定义
// 组件的渲染函数,其返回值必须为虚拟DOM
render() {
// 返回虚拟DOM
return {
type: 'div',
children: '我是文本内容'
}
}
}
| 内容 | 组件选项对象 | 组件实例 |
|---|---|---|
| 来源 | 开发者书写 | Vue 运行时生成 |
| 存在时间 | 编写阶段就存在 | 挂载前创建,卸载后销毁 |
| 作用 | 描述组件的配置 | 提供组件运行时的上下文与功能 |
| 访问方式 | export default {} |
this 或生命周期函数中的上下文 |
| 举例 | data(), methods, mounted() 等 |
this.count, this.$emit, $refs |
3.组件的生命周期
javascript
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type
// 从组件选项对象中取得组件的生命周期函数
const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions
// 在这里调用beforeCreate钩子
beforeCreate && beforeCreate()
const state = reactive(data())
const instance = {
state,
inMounted: false,
subTree: null
}
vnode.component = instance
// 在这里调用created钩子
created && created.call(state)
effect(() => {
const subTree = render.call(state, state)
if(!instance.isMounted) {
// 在这调用beforeMount钩子
beforeMount && beforeMount.call(state)
patch(null, subTree, container, anchor)
instnce.isMounted = true
// 在这里调用mounted钩子
mounted && mounted.call(state)
}
else {
// 在这里调用beforeUpdate钩子
beforeUpdate && beforeUpdate.call(state)
patch(instance.subTree, subTree, container, anchor)
// 在这里调用updated钩子
updated && updated.call(state)
}
instance.subTree = subTree
}, { scheduler: queueJob })
}
4.setup函数的作用与实现
setup函数主要用于配合组合式API,为用户提供一个地方,用于建立组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子等能力。
setup只有在被挂载时执行一次,返回值有两种情况
-
返回一个函数,该函数作为组件的render函数
javascriptconst Comp = { setup() { return () => { return { type: 'div', children: 'hello' } } } } -
返回一个对象,该对象中包含的数据将暴露给模板使用
javascriptconst Comp = { setup() { const count = ref(0) return { count } }, render() { return { type: 'div', children: `count is: ${this.count}` } } }5.props与组件的被动更新
javascriptfunction mountComponent(vnode, container, anchor) { const componentOptions = vnode.type; // 从组件选项对象中取出props定义,即propsOptions const { render, data, props: propsOptions } = componentOptions; beforeCreate && beforeCreate(); const state = reactive(data()); // 解析组件的props数据 const [props, attrs] = resolveProps(propsOptions, vnode.props); const instance = { state, // 将解析出的prpos数据包装为shallowReactive 并定义到组件实例上 props: shallowReactive(props), isMounted: false, subTree: null, }; vnode.component = instance; }
5.组件事件与emit的实现
发射自定义事件的本质就是根据事件名称去props数据对象中寻找对应的事件处理函数并执行
javascript
function emit(event, ...payload) {
// 根据约定对事件名称进行处理,例如change -> onChange
const eventName = `on${event[0].toUpperCase()}${event.slice(1)}`
// 根据处理后的事件名称获取所有的回调函数
const handler = instance.props[eventName]
if(handler) {
// 调用事件处理函数并传递参数
handler(...payload)
} else {
console.warn(`未定义的事件:${event}`)
}
// 将emit函数添加到setupContext中,用户可以通过setupContext取得emit函数
const setupContext = { attrs, emit }
// ...
}
6.插槽的实现
组件模板中的插槽内容会被编译为插槽函数,而插槽函数的返回值就是具体的插槽内容。
实现:只需要将编译好的vnode.children作为slots对象,最后将slots对象添加到setupContext对象中。
11.异步组件与函数式组件
异步组件实现
javascript
function defineAsyncComponent(loader) {
// 一个变量,用来存储异步加载的组件
let InnerComp = null
// 返回一个包装组件
return {
name: 'AsyncComponentWrapper',
setup() {
// 异步组件是否加载成功
const loaded = ref(false)
// 执行加载器函数,返回一个Promise实例
// 加载成功后,将加载成功的组件赋值给InnerComp,并将loaded标记为true,代表加载成功
loader().then(c => {
InnerComp = c
loaded.value = true
})
return () => {
// 如果异步组件加载成功,则渲染该组件,否则渲染一个占位内容
return loaded.value ? { type: InnerComp } : { type: Text, children: '' }
}
}
}
}
12.Vue内置组件实现与原理
内置组件需要渲染器的支持,添加上特殊的判断逻辑
1. Transition组件的实现原理
当DOM元素被挂载时,将动效附加到改DOM元素上;
当DOM元素被卸载时,不要立即卸载DOM元素,而是等到附加到改DOM元素上的动效执行完成后再卸载它
javascript
const Transition = {
name: 'Transition',
setup(props, (slots}) {
return () => {
const innerVNode = slots.default()
// 在过度元素VNode对象上添加transition相应的钩子函数
innerVNode,transition = {
beforeEnter(el) {
}
enter(el) {
},
leave(el, performRemove) {
}
}
}
}
}
Transition 组件本身不会渲染任何额外的内容,它只是通过默认插 槽读取过渡元素,并渲染需要过渡的元素;
Transition 组件的作用,就是在过渡元素的虚拟节点上添加 transition 相关的钩子函数。
javascript
const Transition = {
name: 'Transition',
setup(props, { slots }) {
return () => {
const innerVNode = slots.default()
innerVNode.transition = {
beforeEnter(el) {
// 设置初始状态:添加 enter-from 和 enter-active 类
el.classList.add('enter-from')
el.classList.add('enter-active')
},
enter(el) {
// 在下一帧切换到结束状态
nextFrame(() => {
// 移除 enter-from 类,添加 enter-to 类
el.classList.remove('enter-from')
el.classList.add('enter-to')
// 监听 transitionend 事件完成收尾工作
el.addEventListener('transitionend', () => {
el.classList.remove('enter-to')
el.classList.remove('enter-active')
})
})
},
leave(el, performRemove) {
// 设置离场过渡的初始状态:添加 leave-from 和 leave-active 类
el.classList.add('leave-from')
el.classList.add('leave-active')
// 强制 reflow,使得初始状态生效
document.body.offsetHeight
// 在下一帧修改状态
nextFrame(() => {
// 移除 leave-from 类,添加 leave-to 类
el.classList.remove('leave-from')
el.classList.add('leave-to')
// 监听 transitionend 事件完成收尾工作
el.addEventListener('transitionend', () => {
el.classList.remove('leave-to')
el.classList.remove('leave-active')
// 调用 transition.leave 钩子函数的第二个参数,完成 DOM 元素的卸载
performRemove()
})
})
}
}
return innerVNode
}
}
}
2.Teleport组件的实现原理
该组件可以将指定内容渲染到特定容器中,而不受DOM层级的限制
vue
<template>
<Teleport to="body">
<div class="overlay"></div>
</Teleport>
</template>
<style scoped>
.overlay {
z-index: 9999;
}
</style>
3.KeepAlive组件的实现原理
| 阶段 | 机制 | 作用 |
|---|---|---|
| 缓存结构 | Map<key, vnode> |
存储组件实例 |
| 复用逻辑 | 通过 key 判断是否命中缓存 | 命中则直接复用 component |
| 生命周期控制 | activate() / deactivate() |
移动 DOM 而非销毁 |
| 容量控制 | LRU 策略 | 超出 max 自动删除最旧组件 |
| 过滤规则 | include / exclude | 控制缓存的目标组件 |
javascript
const KeepAlive = {
__isKeepAlive: true,
setup(props, { slots }) {
const cache = new Map();
return () => {
const vnode = slots.default()[0];
const key = vnode.key || vnode.type;
if (cache.has(key)) {
vnode.component = cache.get(key).component;
vnode.shapeFlag |= 256; // COMPONENT_KEPT_ALIVE
} else {
cache.set(key, vnode);
}
vnode.shapeFlag |= 512; // COMPONENT_SHOULD_KEEP_ALIVE
return vnode;
};
}
};
13.编译器核心技术
模板DSL的编译器
源代码--->词法分析--->语法分析--->语义分析--->中间代码生成--->优化--->目标代码生成--->目标代码