虚拟 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函数来创建一个vnodeconst 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.创建新的vnodesetTimeout(() => {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对象上添加挂载的元素属性 elconst el = vnode.el = document.createElement(vnode.tag);// 2.处理propsif (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.处理childrenif (vnode.children) {// 子节点如果是字符串,说明是文本节点,如果不是则是子 VNodeif (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 选择 ProxyProxy 与 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. 保存第一次渲染的 VNodemount(oldVNode, container); // 3. 第一次挂载isMounted = true;} else {const newVNode = rootComponent.render(); // 5. 获取最新的 VNodepatch(oldVNode, newVNode); // 6. patch 局部渲染oldVNode = newVNode; // 7. 新的 VNode 成为旧的 VNode}})}}}
