# Mini-Vue 实现思路
- 渲染系统,改模块主要包含三个功能:
- 功能一:h 函数,用于返回一个 VNode 对象;
- 功能二:mount 函数,用于将 VNode 挂载到 DOM 上;
- 功能三:patch 函数,用于对比两个 VNode 进行对比,决定如何处理新的 VNode;
- 响应式系统:
- 通知依赖 notify ()
- 数据劫持 reactive ()
- 数据结构 getDep ()
- 添加方法 depend ()
- 方法依赖 watchEffect ()
- Vue2 的 defineProperty 实现
- Vue3 的 Proxy 实现
# 渲染器实现
# h 函数与 mount 函数实现
# index
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Render</title> | |
</head> | |
<body> | |
<div id="app"></div> | |
<script src="./render.js"></script> | |
<script> | |
// 1. 通过 h 函数来创建 vnode | |
const vnode1 = h('div', { id: "main" }, [ | |
h("h3", null, "当前计数:100"), | |
h("button", { onclick: () => { console.log("+1"); } }, "+1"), | |
h("button", { onclick: () => { console.log("-1"); } }, "-1") | |
]) // vdom | |
mount(vnode1, document.querySelector("#app")) | |
</script> | |
</body> | |
</html> |
# render
const h = (tag, props, children) => { | |
// 返回一个对象给 vnode | |
return { | |
tag, | |
props, | |
children | |
} | |
} | |
const mount = (vnode, container) => { | |
// 1. 创建出真实的原生,并且在 vnode 上保留 el | |
const el = vnode.el = document.createElement(vnode.tag) | |
// 2. 处理 props | |
if (vnode.props) { | |
for (const key in vnode.props) { | |
const value = vnode.props[key]; | |
// 判断是否是 on 开头的 API 操作 | |
if (key.startsWith("on")) { | |
// 切割并转小写取到 click 并绑定事件 | |
el.addEventListener(key.slice(2).toLowerCase(),vnode.props[key]) | |
} else { | |
// 设置属性 | |
el.setAttribute(key, value) | |
} | |
} | |
} | |
// 3. 处理 children | |
if (vnode.children) { | |
if (typeof vnode.children === 'string' || typeof vnode.children === 'number') { | |
el.textContent = vnode.children | |
} else { | |
// 对数组进行循环 | |
vnode.children.forEach(item => { | |
console.log('item',item); // 打印 vnode 的子元素 | |
// 递归调用 | |
mount(item,el) | |
}) | |
} | |
} | |
// 4. 将 el 挂载到 container 上 | |
container.appendChild(el) | |
return { | |
vnode, | |
container | |
} | |
} |
参考结果:
# patch 函数实现
# index
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Render</title> | |
</head> | |
<body> | |
<div id="app"> | |
<button id="newVNode">创建新的vnode</button> | |
</div> | |
<script src="./render.js"></script> | |
<script> | |
// 1. 通过 h 函数来创建 vnode | |
const vnode1 = h('div', { id: "main" }, [ | |
h("h3", null, `当前计数:100`), | |
h("button", { onclick: () => { console.log("+1"); } }, "+1"), | |
h("button", { onclick: () => { console.log("-1"); } }, "-1") | |
]) // vdom | |
const instanceMount = mount(vnode1, document.querySelector("#app")) | |
console.log(instanceMount); // 可以拿到 mount 的返回值 | |
// 创建新的 vnode | |
const newVNode = document.querySelector("#newVNode") | |
newVNode.onclick = () => { | |
const vnode2 = h('div', { id: "neko", class: 'title' }, [ | |
h("h3", null, `新的VNode`), | |
h("span", null, "新的VNode比旧的Vnode长度短,所以执行patch函数时会将旧的VNode数组进行切割与新数组长度一样"), | |
]) | |
const instancePatch = patch(vnode1, vnode2) | |
console.log(instancePatch); // 可以拿到 patch 的返回值 | |
} | |
</script> | |
</body> | |
</html> |
# render
const h = (tag, props, children) => { | |
// 返回一个对象给 vnode | |
return { | |
tag, | |
props, | |
children | |
} | |
} | |
const mount = (vnode, container) => { | |
// 1. 创建出真实的原生,并且在 vnode 上保留 el | |
const el = vnode.el = document.createElement(vnode.tag) | |
// 2. 处理 props | |
if (vnode.props) { | |
for (const key in vnode.props) { | |
const value = vnode.props[key]; | |
// 对事件监听的判断 | |
if (key.startsWith("on")) { | |
// 切割并转小写取到 click 并绑定事件 | |
el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key]) | |
} else { | |
// 设置属性 | |
el.setAttribute(key, value) | |
} | |
} | |
} | |
// 3. 处理 children | |
if (vnode.children) { | |
if (typeof vnode.children === 'string' || typeof vnode.children === 'number') { | |
el.textContent = vnode.children | |
} else { | |
// 对数组进行循环 | |
vnode.children.forEach(item => { | |
console.log('item', item); // 打印 vnode 的子元素 | |
// 递归调用 | |
mount(item, el) | |
}) | |
} | |
} | |
// 4. 将 el 挂载到 container 上 | |
container.appendChild(el) | |
return { | |
vnode, | |
container | |
} | |
} | |
// n1:vnode1 n2:vnode2 | |
const patch = (n1, n2) => { | |
// 判断类型 | |
if (n1.tag !== n2.tag) { | |
// 获取 n1 的父元素 | |
const n1ElParent = n1.el.parentElement; | |
// 移除 n1 元素 | |
n1ElParent.removeChild(n1.el) | |
// 调用 mount 传入要渲染的元素和渲染的父元素 | |
mount(n2, n1ElParent) | |
} else { // 如果类型一样就判断里面的属性 类似 diff 算法 | |
// 1. 取出 element 对象,并且在 n2 中进行保存 | |
// 此时是吧 n1 原来的元素同时赋值给 n2 | |
const el = n2.el = n1.el | |
// debugger; | |
// 2. 处理 props | |
const oldProps = n1.props || {} | |
const newProps = n2.props || {} | |
// console.log(oldProps); // {id: 'main'} | |
// console.log(newProps); // {class: 'title'} | |
// 2.1. 获取所有的 newProps 添加到 el | |
for (const key in newProps) { | |
const oldValue = oldProps[key]; | |
const newValue = newProps[key]; | |
if (newValue !== oldValue) { | |
// 对事件监听的判断 | |
if (key.startsWith("on")) { | |
// 切割并转小写取到 click 并绑定事件 | |
el.addEventListener(key.slice(2).toLowerCase(), newValue); | |
} else { | |
// 设置属性 | |
el.setAttribute(key, newValue) | |
} | |
} | |
} | |
// 2.2. 删除旧的 props | |
for (const key in oldProps) { | |
// 判断旧的 key 是否存在新的 key 里面 | |
// if (!(key in newProps)) { | |
if (!newProps.hasOwnProperty(key)) { | |
// 判断是否是 on 开头的 API 操作 | |
if (key.startsWith("on")) { | |
const value = oldProps[key] | |
// 移除具体的某一个函数 | |
el.removeEventListener(key.slice(2).toLowerCase(), value) | |
} else { | |
// 移除属性 | |
el.removeAttribute(key) | |
} | |
} | |
} | |
// 3. 处理 children | |
const oldChildren = n1.children || [] | |
const newChildren = n2.children || [] | |
if (typeof newChildren === 'string') { // 情况一:newChildren 本身是一个 string | |
// 边界情况 (edge case) | |
if (typeof oldChildren === 'string') { | |
if (newChildren !== oldChildren) { | |
el.textContent = newChildren | |
} | |
} else { | |
el.innerHTML = newChildren | |
} | |
} else { // 情况二:newChildren 本身是一个数组 | |
if (typeof oldChildren === 'strinf') { | |
el.innerHTML = '' | |
// 遍历新元素里面的 children | |
newChildren.forEach(item => { | |
mount(item,el) | |
}) | |
} else { | |
// oldChildren:[v1,v2,v3] | |
// newChildren:[v1,v2,v3,v4,v5,v6] | |
//commonLength 取两个数组长度更短的那一个值 | |
// 1. 前面有相同节点的元素进行 patch 操作 | |
const commonLength = Math.min(oldChildren.length, newChildren.length) | |
for (let i = 0; i < commonLength; i++){ | |
patch(oldChildren[i],newChildren[i]) // 相等的在这里已经处理完了 | |
} | |
// 2.newChildren.length > oldChildren.length 如果新数组比旧数组长的话,则直接将 oldChildren 切割到与 newChildren 一样长,再直接挂载! | |
if (newChildren.length > oldChildren.length) { | |
newChildren.slice(oldChildren.length).forEach(item => { | |
mount(item,el) | |
}) | |
} | |
// 3.newChildren.length < oldChildren.length 如果旧数组长度比新数组长的话会将剩下的元素切割掉。 | |
if (newChildren.length < oldChildren.length) { | |
oldChildren.slice(newChildren.length).forEach(item => { | |
el.removeChild(item.el) | |
}) | |
} | |
} | |
} | |
} | |
return { | |
n1, | |
n2 | |
} | |
} |
参考结果:
# 响应式系统
# 手动响应式
# index
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Retive</title> | |
</head> | |
<body> | |
<script src="./reactive.js"></script> | |
</body> | |
</html> |
# reactive
class Dep { | |
constructor() { | |
// 不用数组用 Set 是因为元素不会重复 | |
this.subscribers = new Set(); | |
} | |
// 数据发送改变产生的副作用 | |
addEffect(effect) { | |
this.subscribers.add(effect) | |
} | |
// 订阅者通知 | |
notify(effect){ | |
this.subscribers.forEach(effect => { | |
effect() | |
}) | |
} | |
} | |
const dep = new Dep() | |
const info = { counter: 100 } | |
function doubleCounter() { | |
console.log(info.counter * 2) | |
} | |
function powerCounter() { | |
console.log(info.counter * info.counter) | |
} | |
// 手动收集依赖 | |
dep.addEffect(doubleCounter) | |
dep.addEffect(powerCounter) | |
info.counter++; | |
// 手动通知 | |
dep.notify() | |
console.log(dep); |
参考结果:
# effect 代码进行重构 (不能追踪具体属性)
# index
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Retive</title> | |
</head> | |
<body> | |
<script src="./reactive.js"></script> | |
</body> | |
</html> |
# reactive
class Dep { | |
constructor() { | |
// 不用数组用 Set 是因为元素不会重复 | |
this.subscribers = new Set(); | |
} | |
// 数据发送改变产生的副作用 | |
// addEffect(effect) { | |
// this.subscribers.add(effect) | |
// } | |
depend() { | |
if (activeEffect) { | |
this.subscribers.add(activeEffect) | |
} | |
} | |
// 订阅者通知 | |
notify(effect){ | |
this.subscribers.forEach(effect => { | |
effect() | |
}) | |
} | |
} | |
const dep = new Dep() | |
let activeEffect = null; | |
// 执行 dep 某个方法不需要依赖 effect 依然可以添加到订阅者的 set 里面 | |
function watchEffect(effect) { | |
activeEffect = effect; | |
// 执行 depend 自动把 effect 添加到 set 里面 | |
dep.depend(); | |
// 把传进来的函数默认应该执行一次 第一批执行 | |
effect(); | |
//activeEffect 判断完后置空 | |
activeEffect = null; | |
} | |
const info = { counter: 100 } | |
watchEffect(function () { | |
console.log('info.counter * 2:',info.counter * 2) | |
}) | |
watchEffect(function () { | |
console.log('info.counter * info.counter:',info.counter * info.counter) | |
}) | |
watchEffect(function () { | |
console.log('info.counter *+ 10:',info.counter *+ 10) | |
}) | |
// 手动收集依赖 | |
// dep.addEffect(doubleCounter) | |
// dep.addEffect(powerCounter) | |
info.counter++; | |
// 手动通知 | |
dep.notify() | |
console.log(dep); |
参考结果:
# Vue2 defineProperty 实现思路
# index
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Retive</title> | |
</head> | |
<body> | |
<script src="./reactive.js"></script> | |
</body> | |
</html> |
# reactive
class Dep { | |
constructor() { | |
// 不用数组用 Set 是因为元素不会重复 | |
this.subscribers = new Set(); | |
} | |
// 数据发送改变产生的副作用 | |
// addEffect(effect) { | |
// this.subscribers.add(effect) | |
// } | |
depend() { | |
if (activeEffect) { | |
this.subscribers.add(activeEffect) | |
} | |
} | |
// 订阅者通知 | |
notify(effect) { | |
this.subscribers.forEach(effect => { | |
effect() | |
}) | |
} | |
} | |
// const dep = new Dep() | |
let activeEffect = null; | |
// 执行 dep 某个方法不需要依赖 effect 依然可以添加到订阅者的 set 里面 | |
function watchEffect(effect) { | |
activeEffect = effect; | |
// 执行 depend 自动把 effect 添加到 set 里面 | |
//dep.depend (); // 不需要了 因为调用 effect 函数的时候劫持数据已经调用了 depned 了 | |
// 把传进来的函数默认应该执行一次 第一批执行 | |
effect(); | |
//activeEffect 判断完后置空 | |
activeEffect = null; | |
} | |
// Map ({key: value}): key 是一个字符串 | |
// WeakMap ({key (对象): value}):key 是一个对象,弱引用 | |
const targetMap = new WeakMap() | |
function getDep(target, key) { | |
// 1. 根据对象 (target) 取出对应的 Map 对象 | |
let depsMap = targetMap.get(target); | |
if (!depsMap) { | |
depsMap = new Map(); | |
targetMap.set(target, depsMap); | |
} | |
// 2. 取出具体的 Map 对象 | |
let dep = depsMap.get(key); | |
if (!dep) { | |
dep = new Dep(); | |
depsMap.set(key, dep) | |
} | |
return dep; | |
} | |
//vue2 对 raw 进行数据劫持 | |
function reactive(raw) { | |
Object.keys(raw).forEach(key => { | |
const dep = getDep(raw, key); | |
let value = raw[key]; | |
Object.defineProperty(raw, key, { | |
get() { | |
dep.depend(); | |
return value; | |
}, | |
set(newValue) { | |
if (value !== newValue) { | |
value = newValue; | |
dep.notify(); | |
} | |
} | |
}) | |
}) | |
return raw; | |
} | |
// 测试代码 | |
const info = reactive({ counter: 100, name: 'neko' }) | |
const foo = reactive({ height: 1.88 }) | |
// watchEffect1 | |
watchEffect(function () { | |
console.log('effect1:', info.counter * 2, "info.name:", info.name) | |
}) | |
// watchEffect2 | |
watchEffect(function () { | |
console.log('effect2:', info.counter * info.counter) | |
}) | |
// watchEffect3 | |
watchEffect(function () { | |
console.log('effect3:', info.counter * + 10, "info.name:", info.name) | |
}) | |
// watchEffect4 | |
watchEffect(function () { | |
console.log('effect4:', foo.height) | |
}) | |
// 手动收集依赖 | |
// dep.addEffect(doubleCounter) | |
// dep.addEffect(powerCounter) | |
info.counter++; | |
// 手动通知 | |
// dep.notify() | |
// 数据发生修改需要进行劫持 | |
const p = new Promise((resolve, reject) => { | |
setTimeout(() => { | |
console.log('-----------2s后info.name发生了修改-----------'); | |
//info.name = 'neko' //name 未发生改变时不用执行赋值与通知 | |
info.name = 'saber' | |
resolve("name已发生改变可以,2秒后改变foo.height会触发effect4收集依赖") | |
}, 2000) | |
}) | |
p.then(res => { | |
console.log(res); | |
setTimeout(() => { | |
foo.height = 1.68 | |
}, 2000) | |
}) | |
// foo.height = 2 | |
// console.log(dep); |
参考结果:
# Vue3 Proxy 实现思路
# index
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Retive</title> | |
</head> | |
<body> | |
<script src="./reactive.js"></script> | |
</body> | |
</html> |
# reactive
class Dep { | |
constructor() { | |
// 不用数组用 Set 是因为元素不会重复 | |
this.subscribers = new Set(); | |
} | |
// 数据发送改变产生的副作用 | |
// addEffect(effect) { | |
// this.subscribers.add(effect) | |
// } | |
depend() { | |
if (activeEffect) { | |
this.subscribers.add(activeEffect) | |
} | |
} | |
// 订阅者通知 | |
notify(effect) { | |
this.subscribers.forEach(effect => { | |
effect() | |
}) | |
} | |
} | |
// const dep = new Dep() | |
let activeEffect = null; | |
// 执行 dep 某个方法不需要依赖 effect 依然可以添加到订阅者的 set 里面 | |
function watchEffect(effect) { | |
activeEffect = effect; | |
// 执行 depend 自动把 effect 添加到 set 里面 | |
//dep.depend (); // 不需要了 因为调用 effect 函数的时候劫持数据已经调用了 depned 了 | |
// 把传进来的函数默认应该执行一次 第一批执行 | |
effect(); | |
//activeEffect 判断完后置空 | |
activeEffect = null; | |
} | |
// Map ({key: value}): key 是一个字符串 | |
// WeakMap ({key (对象): value}):key 是一个对象,弱引用 | |
const targetMap = new WeakMap() | |
function getDep(target, key) { | |
// 1. 根据对象 (target) 取出对应的 Map 对象 | |
let depsMap = targetMap.get(target); | |
if (!depsMap) { | |
depsMap = new Map(); | |
targetMap.set(target, depsMap); | |
} | |
// 2. 取出具体的 Map 对象 | |
let dep = depsMap.get(key); | |
if (!dep) { | |
dep = new Dep(); | |
depsMap.set(key, dep) | |
} | |
return dep; | |
} | |
//vue3 对 raw 进行数据劫持 | |
function reactive(raw) { | |
return new Proxy(raw, { | |
get(target, key) { | |
const dep = getDep(target, key) | |
dep.depend(); | |
return target[key]; | |
}, | |
set(target, key, newValue) { | |
const dep = getDep(target, key); | |
target[key] = newValue; | |
dep.notify(); | |
} | |
}); | |
} | |
// 测试代码 | |
const info = reactive({ counter: 100, name: 'neko' }) | |
const foo = reactive({ height: 1.88 }) | |
// watchEffect1 | |
watchEffect(function () { | |
console.log('effect1:', info.counter * 2, "info.name:", info.name) | |
}) | |
// watchEffect2 | |
watchEffect(function () { | |
console.log('effect2:', info.counter * info.counter) | |
}) | |
// watchEffect3 | |
watchEffect(function () { | |
console.log('effect3:', info.counter * + 10, "info.name:", info.name) | |
}) | |
// watchEffect4 | |
watchEffect(function () { | |
console.log('effect4:', foo.height) | |
}) | |
// 手动收集依赖 | |
// dep.addEffect(doubleCounter) | |
// dep.addEffect(powerCounter) | |
info.counter++; | |
// 手动通知 | |
// dep.notify() | |
// 数据发生修改需要进行劫持 | |
const p = new Promise((resolve, reject) => { | |
setTimeout(() => { | |
console.log('-----------2s后info.name发生了修改-----------'); | |
//info.name = 'neko' //name 未发生改变时不用执行赋值与通知 | |
info.name = 'saber' | |
resolve("name已发生改变可以,2秒后改变foo.height会触发effect4收集依赖") | |
}, 2000) | |
}) | |
p.then(res => { | |
console.log(res); | |
setTimeout(() => { | |
foo.height = 1.68 | |
}, 2000) | |
}) | |
// foo.height = 2 | |
// console.log(dep); |
参考结果:也能实现上面的效果,但无疑使用 Proxy 更好!
# Mini-Vue 实现结果
# index html
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Mini Vue实现</title> | |
</head> | |
<body> | |
<div id="app"></div> | |
<script src="./render.js"></script> | |
<script src="./reactive.js"></script> | |
<script src="./index.js"></script> | |
<script> | |
// 1. 创建根组件 | |
const App = { | |
data: reactive({ | |
counter: 0 | |
}), | |
render() { | |
return h("div", null, [ | |
h('h2', null, `当前计数:${this.data.counter}`), | |
h("button", { | |
onClick: () => { | |
this.data.counter++ | |
} | |
}, "+1") | |
]) | |
} | |
} | |
// 2. 挂载根组件 | |
const app = createApp(App) | |
app.mount("#app") | |
</script> | |
</body> | |
</html> |
# render (改进)
const h = (tag, props, children) => { | |
// 返回一个对象给 vnode | |
return { | |
tag, | |
props, | |
children | |
} | |
} | |
const mount = (vnode, container) => { | |
// 1. 创建出真实的原生,并且在 vnode 上保留 el | |
const el = vnode.el = document.createElement(vnode.tag) | |
// 2. 处理 props | |
if (vnode.props) { | |
for (const key in vnode.props) { | |
const value = vnode.props[key]; | |
// 对事件监听的判断 | |
if (key.startsWith("on")) { | |
// 切割并转小写取到 click 并绑定事件 | |
el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key]) | |
} else { | |
// 设置属性 | |
el.setAttribute(key, value) | |
} | |
} | |
} | |
// 3. 处理 children | |
if (vnode.children) { | |
if (typeof vnode.children === 'string' || typeof vnode.children === 'number') { | |
el.textContent = vnode.children | |
} else { | |
// 对数组进行循环 | |
vnode.children.forEach(item => { | |
console.log('item', item); // 打印 vnode 的子元素 | |
// 递归调用 | |
mount(item, el) | |
}) | |
} | |
} | |
// 4. 将 el 挂载到 container 上 | |
container.appendChild(el) | |
return { | |
vnode, | |
container | |
} | |
} | |
// n1:vnode1 n2:vnode2 | |
const patch = (n1, n2) => { | |
// 判断类型 | |
if (n1.tag !== n2.tag) { | |
// 获取 n1 的父元素 | |
const n1ElParent = n1.el.parentElement; | |
// 移除 n1 元素 | |
n1ElParent.removeChild(n1.el) | |
// 调用 mount 传入要渲染的元素和渲染的父元素 | |
mount(n2, n1ElParent) | |
} else { // 如果类型一样就判断里面的属性 类似 diff 算法 | |
// 1. 取出 element 对象,并且在 n2 中进行保存 | |
// 此时是吧 n1 原来的元素同时赋值给 n2 | |
const el = n2.el = n1.el | |
// debugger; | |
// 2. 处理 props | |
const oldProps = n1.props || {} | |
const newProps = n2.props || {} | |
// console.log(oldProps); // {id: 'main'} | |
// console.log(newProps); // {class: 'title'} | |
// 2.1. 获取所有的 newProps 添加到 el | |
for (const key in newProps) { | |
const oldValue = oldProps[key]; | |
const newValue = newProps[key]; | |
if (newValue !== oldValue) { | |
// 对事件监听的判断 | |
if (key.startsWith("on")) { | |
// 切割并转小写取到 click 并绑定事件 | |
el.addEventListener(key.slice(2).toLowerCase(), newValue); | |
} else { | |
// 设置属性 | |
el.setAttribute(key, newValue) | |
} | |
} | |
} | |
// 2.2. 删除旧的 props | |
for (const key in oldProps) { | |
// 判断旧的 key 是否存在新的 key 里面 | |
// if (!(key in newProps)) { | |
//if (!newProps.hasOwnProperty (key)) { // 这里移除了判断 因为每次执行 patch 都添加了事件 每次判断取到的对象不是同一个对象 | |
// 每次判断这里的 key 是以 on 开头的情况下都执行移除 | |
if (key.startsWith("on")) { | |
const value = oldProps[key] | |
// 移除具体的某一个函数 | |
el.removeEventListener(key.slice(2).toLowerCase(), value) | |
} | |
if(!(key in newProps)) { | |
// 移除属性 | |
el.removeAttribute(key); | |
} | |
// } | |
} | |
// 3. 处理 children | |
const oldChildren = n1.children || [] | |
const newChildren = n2.children || [] | |
if (typeof newChildren === 'string') { // 情况一:newChildren 本身是一个 string | |
// 边界情况 (edge case) | |
if (typeof oldChildren === 'string') { | |
if (newChildren !== oldChildren) { | |
el.textContent = newChildren | |
} | |
} else { | |
el.innerHTML = newChildren | |
} | |
} else { // 情况二:newChildren 本身是一个数组 | |
if (typeof oldChildren === 'strinf') { | |
el.innerHTML = '' | |
// 遍历新元素里面的 children | |
newChildren.forEach(item => { | |
mount(item,el) | |
}) | |
} else { | |
// oldChildren:[v1,v2,v3] | |
// newChildren:[v1,v2,v3,v4,v5,v6] | |
//commonLength 取两个数组长度更短的那一个值 | |
// 1. 前面有相同节点的元素进行 patch 操作 | |
const commonLength = Math.min(oldChildren.length, newChildren.length) | |
for (let i = 0; i < commonLength; i++){ | |
patch(oldChildren[i],newChildren[i]) // 相等的在这里已经处理完了 | |
} | |
// 2.newChildren.length > oldChildren.length 如果新数组比旧数组长的话,则直接将 oldChildren 切割到与 newChildren 一样长,再直接挂载! | |
if (newChildren.length > oldChildren.length) { | |
newChildren.slice(oldChildren.length).forEach(item => { | |
mount(item,el) | |
}) | |
} | |
// 3.newChildren.length < oldChildren.length 如果旧数组长度比新数组长的话会将剩下的元素切割掉。 | |
if (newChildren.length < oldChildren.length) { | |
oldChildren.slice(newChildren.length).forEach(item => { | |
el.removeChild(item.el) | |
}) | |
} | |
} | |
} | |
} | |
return { | |
n1, | |
n2 | |
} | |
} |
# reactive (Proxy)
class Dep { | |
constructor() { | |
// 不用数组用 Set 是因为元素不会重复 | |
this.subscribers = new Set(); | |
} | |
// 数据发送改变产生的副作用 | |
// addEffect(effect) { | |
// this.subscribers.add(effect) | |
// } | |
depend() { | |
if (activeEffect) { | |
this.subscribers.add(activeEffect) | |
} | |
} | |
// 订阅者通知 | |
notify(effect) { | |
this.subscribers.forEach(effect => { | |
effect() | |
}) | |
} | |
} | |
// const dep = new Dep() | |
let activeEffect = null; | |
// 执行 dep 某个方法不需要依赖 effect 依然可以添加到订阅者的 set 里面 | |
function watchEffect(effect) { | |
activeEffect = effect; | |
// 执行 depend 自动把 effect 添加到 set 里面 | |
//dep.depend (); // 不需要了 因为调用 effect 函数的时候劫持数据已经调用了 depned 了 | |
// 把传进来的函数默认应该执行一次 第一批执行 | |
effect(); | |
//activeEffect 判断完后置空 | |
activeEffect = null; | |
} | |
// Map ({key: value}): key 是一个字符串 | |
// WeakMap ({key (对象): value}):key 是一个对象,弱引用 | |
const targetMap = new WeakMap() | |
function getDep(target, key) { | |
// 1. 根据对象 (target) 取出对应的 Map 对象 | |
let depsMap = targetMap.get(target); | |
if (!depsMap) { | |
depsMap = new Map(); | |
targetMap.set(target, depsMap); | |
} | |
// 2. 取出具体的 Map 对象 | |
let dep = depsMap.get(key); | |
if (!dep) { | |
dep = new Dep(); | |
depsMap.set(key, dep) | |
} | |
return dep; | |
} | |
//vue3 对 raw 进行数据劫持 | |
function reactive(raw) { | |
return new Proxy(raw, { | |
get(target, key) { | |
const dep = getDep(target, key) | |
dep.depend(); | |
return target[key]; | |
}, | |
set(target, key, newValue) { | |
const dep = getDep(target, key); | |
target[key] = newValue; | |
dep.notify(); | |
} | |
}); | |
} | |
// 测试代码 | |
const info = reactive({ counter: 100, name: 'neko' }) | |
const foo = reactive({ height: 1.88 }) | |
// watchEffect1 | |
watchEffect(function () { | |
console.log('effect1:', info.counter * 2, "info.name:", info.name) | |
}) | |
// watchEffect2 | |
watchEffect(function () { | |
console.log('effect2:', info.counter * info.counter) | |
}) | |
// watchEffect3 | |
watchEffect(function () { | |
console.log('effect3:', info.counter * + 10, "info.name:", info.name) | |
}) | |
// watchEffect4 | |
watchEffect(function () { | |
console.log('effect4:', foo.height) | |
}) | |
// 手动收集依赖 | |
// dep.addEffect(doubleCounter) | |
// dep.addEffect(powerCounter) | |
info.counter++; | |
// 手动通知 | |
// dep.notify() | |
// 数据发生修改需要进行劫持 | |
const p = new Promise((resolve, reject) => { | |
setTimeout(() => { | |
console.log('-----------2s后info.name发生了修改-----------'); | |
//info.name = 'neko' //name 未发生改变时不用执行赋值与通知 | |
info.name = 'saber' | |
resolve("name已发生改变可以,2秒后改变foo.height会触发effect4收集依赖") | |
}, 2000) | |
}) | |
p.then(res => { | |
console.log(res); | |
setTimeout(() => { | |
foo.height = 1.68 | |
}, 2000) | |
}) | |
// foo.height = 2 | |
// console.log(dep); |
# index js
function createApp(rootComponent) { | |
return { | |
// 选择器 | |
mount(selector) { | |
const container = document.querySelector(selector); | |
let isMounted = false; | |
let oldVNode = null; | |
// 数据发送更新 | |
watchEffect(() => { | |
// 没有挂载的情况下 调用 render 里面的 mount | |
if (!isMounted) { | |
oldVNode = rootComponent.render() | |
mount(oldVNode, container); | |
// 下次已经挂载完了 | |
isMounted = true; | |
} else { | |
const newVNode = rootComponent.render(); | |
patch(oldVNode, newVNode); | |
oldVNode = newVNode; | |
} | |
}) | |
} | |
} | |
} |
参考结果: