虚拟 DOM
虚拟 dom 的优点:
- 渲染优化
- 可以异步的处理数据,比如循环生成一百个标签,循环操作的是虚拟 dom,渲染时,只会将最终结果渲染一次。
- 更好的兼容性,提供跨平台能力
- Compiler模块:编译模板系统;
- 编译模块主要将 vue 代码转成 AST,再转成 js,所以里面有大量的正则。
- 它就像一个翻译,翻译完后就不用了,而且还可以选择其他的翻译者。
- 比如脚手架中 .vue 文件其实是通过一些插件如
@vue/compiler-sfc
来编译。 - 并且编译模块代码实际也不会跑在用户浏览器中。
- 比如脚手架中 .vue 文件其实是通过一些插件如
- Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
- Reactivity模块:响应式系统;
三大系统协同工作
mini-vue
这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:
<!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>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./renderer.js"></script>
<script>
// 1.通过h函数来创建一个vnode
const vnode = h('div', {class: "why", id: "aaa"}, [
h("h2", null, "当前计数: 100"),
h("button", {onClick: function() {}}, "+1")
]); // vdom
// 2.通过mount函数, 将vnode挂载到div#app上
mount(vnode, document.querySelector("#app"))
// 3.创建新的vnode
setTimeout(() => {
const vnode1 = h('div', {class: "coderwhy", id: "aaa"}, [
h("h2", null, "呵呵呵"),
h("button", {onClick: function() {}}, "-1")
]);
patch(vnode, vnode1);
}, 2000)
</script>
</body>
</html>
const h = (tag, props, children) => {
// vnode -> javascript对象 -> {}
return {
tag,
props,
children
}
}
mount 函数 - 挂载VNode
mount 函数将 VNode 转成真实 dom,然后挂载到容器元素中。
const mount = (vnode, container) => {
// vnode -> element
// 1.创建出真实的原生dom, 并且在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];
// 如果 prop 是监听事件,则添加监听事件,反之添加 attribute 属性
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value);
}
}
}
// 3.处理children
if (vnode.children) {
// 子节点如果是字符串,说明是文本节点,如果不是则是子 VNode
if (typeof vnode.children === "string") {
el.textContent = vnode.children;
} else {
vnode.children.forEach(item => {
mount(item, el); // 递归挂载
})
}
}
// 4.将生成的真实dom元素 el 挂载到container上
container.appendChild(el);
}
patch 函数 - 对比两个VNode
patch 函数就是 diff 两个 VNode 的差异,然后进行局部渲染。
注意:mini-vue 没有考虑边界情况,比如 VNode children 可能是插槽,tag 可能是组件等等。
这里只考虑 children 为最常见的字符串或者保存普通元素节点的数组。
patch 过程:
- 如果两个 VNode 类型都不一样,比如 h3 和 div。那直接移除老节点,将新VNode挂载上去。
- 如果类型一样,则统一标签元素 el,并开始比较 props。
- 将新节点的 props 添加到 el 中,添加的时候进行比较。
- 比较依据就是当 key 相同的时候,value 是否相同。
- 如果老节点没有新节点的 key,那 value 肯定不相同,setAttribute() 就是在设置一个新的属性
- 如果新老节点都有这个 key,但值不相同,则 setAttribute 就是在将新节点的 value 覆盖更新上去
- 注意:value 如果是函数,函数可没有 setAttribute 能进行覆盖,addEventListener 可以添加重复事件,也就是老节点的事件 prop 没有被覆盖,el 上出现两个相同事件。
- 最后可能还有 props 是老节点,而新节点没有的,通过 in 判断,遍历所有老节点的 key,只要不在新节点上的直接删除。
- 另外还要删除所有在老节点上的事件 prop,因为他们要么和新节点重复了,要么就是新节点压根没有这个事件。
比较 children
- 如果新老节点 children 都为字符串,也就是节点内容为文本,那可以直接替换
- 如果老节点 children 是字符串,但是新节点不是,则直接删掉老节点的 children 内容,将新节点 children 挂载上去
如果新老节点 children 都不是字符串,则要开始 diff 比较。假设这里都为数组。
- 按道理要比较两个数组中的 VNode ,按照节点的 key 比较效率最高,
- 因为可以很快找到相同的节点,再进行详细比较。这也是为什么需要唯一的 key 的原因。
- 没有 key 只能按数组顺序一个一个比过去。
- 比较的方式为先比较两个数组的长度。
- 公共的部分,按顺序一一对应进行 patch
- 如果新节点 children 数组比较长,则多出来的节点直接挂载上去
- 如果老节点数组比较上,则多出来的节点直接删掉 ```javascript const patch = (n1, n2) => { if (n1.tag !== n2.tag) { const n1ElParent = n1.el.parentElement; n1ElParent.removeChild(n1.el); mount(n2, n1ElParent); } else { // 1.取出element对象, 并且在n2中进行保存 const el = n2.el = n1.el; // 都是引用,实际指向都是同一个dom元素
// 2.处理props const oldProps = n1.props || {}; const newProps = n2.props || {}; // 2.1.获取所有的newProps添加到el for (const key in newProps) { const oldValue = oldProps[key]; const newValue = newProps[key]; if (newValue !== oldValue) { if (key.startsWith(“on”)) { // 对事件监听的判断
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue);
} } }
// 2.2.删除旧的props for (const key in oldProps) { 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 newChidlren = n2.children || [];
if (typeof newChidlren === “string”) { // 情况一: newChildren本身是一个string // 边界情况 (edge case) if (typeof oldChildren === “string”) { if (newChidlren !== oldChildren) {
el.textContent = newChidlren
} } else { el.innerHTML = newChidlren; } } else { // 情况二: newChildren本身是一个数组 if (typeof oldChildren === “string”) { el.innerHTML = “”; newChidlren.forEach(item => {
mount(item, el);
}) } else { // oldChildren: [v1, v2, v3, v8, v9] // newChildren: [v1, v5, v6] // 1.前面有相同节点的原生进行patch操作 const commonLength = Math.min(oldChildren.length, newChidlren.length); for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChidlren[i]);
}
// 2.newChildren.length > oldChildren.length if (newChidlren.length > oldChildren.length) {
newChidlren.slice(oldChildren.length).forEach(item => {
mount(item, el);
})
}
// 3.newChildren.length < oldChildren.length if (newChidlren.length < oldChildren.length) {
oldChildren.slice(newChidlren.length).forEach(item => {
el.removeChild(item.el);
})
响应式系统
响应式原理
15. Proxy-Reflect 响应式原理 ```javascript class Dep { constructor() { this.subscribers = new Set(); }
- 按道理要比较两个数组中的 VNode ,按照节点的 key 比较效率最高,
depend() { if (activeEffect) { this.subscribers.add(activeEffect); } }
notify() { this.subscribers.forEach(effect => { effect(); }) } }
let activeEffect = null; function watchEffect(effect) { activeEffect = effect; effect(); 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.取出具体的dep对象 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 proxy = reactive({name: “123”}) // proxy.name = “321”;
// // 测试代码 // const info = reactive({counter: 100, name: “why”}); // const foo = reactive({height: 1.88});
// // watchEffect1 // watchEffect(function () { // console.log(“effect1:”, info.counter * 2, info.name); // })
// // watchEffect2 // watchEffect(function () { // console.log(“effect2:”, info.counter * info.counter); // })
// // watchEffect3 // watchEffect(function () { // console.log(“effect3:”, info.counter + 10, info.name); // })
// watchEffect(function () { // console.log(“effect4:”, foo.height); // })
// info.counter++; // info.name = “why”;
// foo.height = 2;
<a name="SiKko"></a>
### 为什么 Vue3 选择 Proxy
Proxy 与 Object.definedProperty.相比:
1. **对于新增或删除对象,Proxy 劫持的是整个对象,不需要做特殊处理;**
- Object.definedProperty 是劫持对象的属性时,如果新增元素:那么Vue2需要再次调用definedProperty,
2. **修改对象的不同:**
- 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
- 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
3. **Proxy 能观察的类型比 defineProperty 更丰富**
- has:in操作符的捕获器;
- deleteProperty:delete 操作符的捕捉器;
- 等等其他操作;
4. **Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;**
缺点:Proxy 不兼容IE,也没有 polyfill(补丁), defineProperty 能支持到IE9
<a name="ik091"></a>
## 框架外层API设计
应用程序入口模块。从框架的层面来说,我们需要有两部分内容:
1. createApp用于创建一个app对象;
2. 该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上;
```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>Document</title>
</head>
<body>
<div id="app"></div>
<script src="../02_渲染器实现/renderer.js"></script>
<script src="../03_响应式系统/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++
console.log(this.data.counter);
}
}, "+1")
])
}
}
// 2.挂载根组件
const app = createApp(App);
app.mount("#app");
</script>
</body>
</html>
function createApp(rootComponent) {
return {
// 1. createApp 返回的对象中定义挂载函数,函数接收一个选择器
mount(selector) {
const container = document.querySelector(selector);
let isMounted = false; // 标识第一次渲染,还是后续渲染,后续渲染是 patch 操作
let oldVNode = null;
// 4. 第一次渲染后,数据更改,就要触发 patch,所以要收集数据的依赖进行响应式 patch。
watchEffect(function() {
if (!isMounted) {
oldVNode = rootComponent.render(); // 2. 保存第一次渲染的 VNode
mount(oldVNode, container); // 3. 第一次挂载
isMounted = true;
} else {
const newVNode = rootComponent.render(); // 5. 获取最新的 VNode
patch(oldVNode, newVNode); // 6. patch 局部渲染
oldVNode = newVNode; // 7. 新的 VNode 成为旧的 VNode
}
})
}
}
}