虚拟 DOM

image.png
虚拟 dom 的优点:

  1. 渲染优化
    1. 可以异步的处理数据,比如循环生成一百个标签,循环操作的是虚拟 dom,渲染时,只会将最终结果渲染一次。
  2. 更好的兼容性,提供跨平台能力
    1. 兼容性:不同的 dom 能触发的事件在浏览器底层已经定死了,而虚拟 dom 能提供统一的 api,抹平不同浏览器之间的事件差异。
    2. 跨平台:虚拟 dom 是一个抽离的公共层,这为跨平台就提供了可能。

      vue 架构

      三大核心系统

      事实上Vue的源码包含三大核心:
  • Compiler模块:编译模板系统;
    • 编译模块主要将 vue 代码转成 AST,再转成 js,所以里面有大量的正则。
    • 它就像一个翻译,翻译完后就不用了,而且还可以选择其他的翻译者。
      • 比如脚手架中 .vue 文件其实是通过一些插件如@vue/compiler-sfc来编译。
      • 并且编译模块代码实际也不会跑在用户浏览器中。
  • Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
  • Reactivity模块:响应式系统;

image.png

三大系统协同工作

image.png

mini-vue

这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:

  • 渲染系统模块;
  • 可响应式系统模块;
  • 应用程序入口模块;

    渲染系统

    h 函数 - 生成 VNode

    h 函数主要是创建虚拟节点。

12. 高级语法补充

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Document</title>
  8. </head>
  9. <body>
  10. <div id="app"></div>
  11. <script src="./renderer.js"></script>
  12. <script>
  13. // 1.通过h函数来创建一个vnode
  14. const vnode = h('div', {class: "why", id: "aaa"}, [
  15. h("h2", null, "当前计数: 100"),
  16. h("button", {onClick: function() {}}, "+1")
  17. ]); // vdom
  18. // 2.通过mount函数, 将vnode挂载到div#app上
  19. mount(vnode, document.querySelector("#app"))
  20. // 3.创建新的vnode
  21. setTimeout(() => {
  22. const vnode1 = h('div', {class: "coderwhy", id: "aaa"}, [
  23. h("h2", null, "呵呵呵"),
  24. h("button", {onClick: function() {}}, "-1")
  25. ]);
  26. patch(vnode, vnode1);
  27. }, 2000)
  28. </script>
  29. </body>
  30. </html>
  1. const h = (tag, props, children) => {
  2. // vnode -> javascript对象 -> {}
  3. return {
  4. tag,
  5. props,
  6. children
  7. }
  8. }

mount 函数 - 挂载VNode

mount 函数将 VNode 转成真实 dom,然后挂载到容器元素中。

  1. const mount = (vnode, container) => {
  2. // vnode -> element
  3. // 1.创建出真实的原生dom, 并且在vnode对象上添加挂载的元素属性 el
  4. const el = vnode.el = document.createElement(vnode.tag);
  5. // 2.处理props
  6. if (vnode.props) {
  7. for (const key in vnode.props) {
  8. const value = vnode.props[key];
  9. // 如果 prop 是监听事件,则添加监听事件,反之添加 attribute 属性
  10. if (key.startsWith("on")) {
  11. el.addEventListener(key.slice(2).toLowerCase(), value)
  12. } else {
  13. el.setAttribute(key, value);
  14. }
  15. }
  16. }
  17. // 3.处理children
  18. if (vnode.children) {
  19. // 子节点如果是字符串,说明是文本节点,如果不是则是子 VNode
  20. if (typeof vnode.children === "string") {
  21. el.textContent = vnode.children;
  22. } else {
  23. vnode.children.forEach(item => {
  24. mount(item, el); // 递归挂载
  25. })
  26. }
  27. }
  28. // 4.将生成的真实dom元素 el 挂载到container上
  29. container.appendChild(el);
  30. }

patch 函数 - 对比两个VNode

patch 函数就是 diff 两个 VNode 的差异,然后进行局部渲染。

注意:mini-vue 没有考虑边界情况,比如 VNode children 可能是插槽,tag 可能是组件等等。
这里只考虑 children 为最常见的字符串或者保存普通元素节点的数组。

patch 过程:

  1. 如果两个 VNode 类型都不一样,比如 h3 和 div。那直接移除老节点,将新VNode挂载上去。
  2. 如果类型一样,则统一标签元素 el,并开始比较 props。
    1. 将新节点的 props 添加到 el 中,添加的时候进行比较。
    2. 比较依据就是当 key 相同的时候,value 是否相同。
      1. 如果老节点没有新节点的 key,那 value 肯定不相同,setAttribute() 就是在设置一个新的属性
      2. 如果新老节点都有这个 key,但值不相同,则 setAttribute 就是在将新节点的 value 覆盖更新上去
      • 注意:value 如果是函数,函数可没有 setAttribute 能进行覆盖,addEventListener 可以添加重复事件,也就是老节点的事件 prop 没有被覆盖,el 上出现两个相同事件。
    3. 最后可能还有 props 是老节点,而新节点没有的,通过 in 判断,遍历所有老节点的 key,只要不在新节点上的直接删除。
      1. 另外还要删除所有在老节点上的事件 prop,因为他们要么和新节点重复了,要么就是新节点压根没有这个事件。
  3. 比较 children

    1. 如果新老节点 children 都为字符串,也就是节点内容为文本,那可以直接替换
    2. 如果老节点 children 是字符串,但是新节点不是,则直接删掉老节点的 children 内容,将新节点 children 挂载上去
    3. 如果新老节点 children 都不是字符串,则要开始 diff 比较。假设这里都为数组。

      1. 按道理要比较两个数组中的 VNode ,按照节点的 key 比较效率最高,
        1. 因为可以很快找到相同的节点,再进行详细比较。这也是为什么需要唯一的 key 的原因。
        2. 没有 key 只能按数组顺序一个一个比过去。
      2. 比较的方式为先比较两个数组的长度。
      3. 公共的部分,按顺序一一对应进行 patch
      4. 如果新节点 children 数组比较长,则多出来的节点直接挂载上去
      5. 如果老节点数组比较上,则多出来的节点直接删掉 ```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”)) { // 对事件监听的判断

      1. el.addEventListener(key.slice(2).toLowerCase(), newValue)

      } else {

      1. 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) {

      1. el.textContent = newChidlren

      } } else { el.innerHTML = newChidlren; } } else { // 情况二: newChildren本身是一个数组 if (typeof oldChildren === “string”) { el.innerHTML = “”; newChidlren.forEach(item => {

      1. 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++) {

      1. patch(oldChildren[i], newChidlren[i]);

      }

      // 2.newChildren.length > oldChildren.length if (newChidlren.length > oldChildren.length) {

      1. newChidlren.slice(oldChildren.length).forEach(item => {
      2. mount(item, el);
      3. })

      }

      // 3.newChildren.length < oldChildren.length if (newChidlren.length < oldChildren.length) {

      1. oldChildren.slice(newChidlren.length).forEach(item => {
      2. el.removeChild(item.el);
      3. })

      } } } } } ```

      响应式系统

      响应式原理

      15. Proxy-Reflect 响应式原理 ```javascript class Dep { constructor() { this.subscribers = new Set(); }

    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;

  1. <a name="SiKko"></a>
  2. ### 为什么 Vue3 选择 Proxy
  3. Proxy 与 Object.definedProperty.相比:
  4. 1. **对于新增或删除对象,Proxy 劫持的是整个对象,不需要做特殊处理;**
  5. - Object.definedProperty 是劫持对象的属性时,如果新增元素:那么Vue2需要再次调用definedProperty,
  6. 2. **修改对象的不同:**
  7. - 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截;
  8. - 而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截;
  9. 3. **Proxy 能观察的类型比 defineProperty 更丰富**
  10. - has:in操作符的捕获器;
  11. - deleteProperty:delete 操作符的捕捉器;
  12. - 等等其他操作;
  13. 4. **Proxy 作为新标准将受到浏览器厂商重点持续的性能优化;**
  14. 缺点:Proxy 不兼容IE,也没有 polyfill(补丁), defineProperty 能支持到IE9
  15. <a name="ik091"></a>
  16. ## 框架外层API设计
  17. 应用程序入口模块。从框架的层面来说,我们需要有两部分内容:
  18. 1. createApp用于创建一个app对象;
  19. 2. 该app对象有一个mount方法,可以将根组件挂载到某一个dom元素上;
  20. ```html
  21. <!DOCTYPE html>
  22. <html lang="en">
  23. <head>
  24. <meta charset="UTF-8">
  25. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  26. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  27. <title>Document</title>
  28. </head>
  29. <body>
  30. <div id="app"></div>
  31. <script src="../02_渲染器实现/renderer.js"></script>
  32. <script src="../03_响应式系统/reactive.js"></script>
  33. <script src="./index.js"></script>
  34. <script>
  35. // 1.创建根组件
  36. const App = {
  37. data: reactive({
  38. counter: 0
  39. }),
  40. render() {
  41. return h("div", null, [
  42. h("h2", null, `当前计数: ${this.data.counter}`),
  43. h("button", {
  44. onClick: () => {
  45. this.data.counter++
  46. console.log(this.data.counter);
  47. }
  48. }, "+1")
  49. ])
  50. }
  51. }
  52. // 2.挂载根组件
  53. const app = createApp(App);
  54. app.mount("#app");
  55. </script>
  56. </body>
  57. </html>
  1. function createApp(rootComponent) {
  2. return {
  3. // 1. createApp 返回的对象中定义挂载函数,函数接收一个选择器
  4. mount(selector) {
  5. const container = document.querySelector(selector);
  6. let isMounted = false; // 标识第一次渲染,还是后续渲染,后续渲染是 patch 操作
  7. let oldVNode = null;
  8. // 4. 第一次渲染后,数据更改,就要触发 patch,所以要收集数据的依赖进行响应式 patch。
  9. watchEffect(function() {
  10. if (!isMounted) {
  11. oldVNode = rootComponent.render(); // 2. 保存第一次渲染的 VNode
  12. mount(oldVNode, container); // 3. 第一次挂载
  13. isMounted = true;
  14. } else {
  15. const newVNode = rootComponent.render(); // 5. 获取最新的 VNode
  16. patch(oldVNode, newVNode); // 6. patch 局部渲染
  17. oldVNode = newVNode; // 7. 新的 VNode 成为旧的 VNode
  18. }
  19. })
  20. }
  21. }
  22. }

阅读源码