当前位置:   article > 正文

Vue3源码学习之旅(1)--自己实现一个简单的渲染系统-render/h/patch函数_vue render函数 patchflag

vue render函数 patchflag

Vue3源码学习

真实的DOM渲染

我们传统的前端开发中,我们是编写自己的HTML,最终被渲染到浏览器上的,那么它是什么样的过程呢?

image-20210825220723339

虚拟DOM的优势

目前框架都会引入虚拟DOM来对真实的DOM进行抽象,这样做有很多的好处:

首先是可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作:

  • p因为对于直接操作DOM来说是有很多的限制的,比如diff、clone等等,但是使用JavaScript编程语言来操作这 些,就变得非常的简单;
  • 我们可以使用JavaScript来表达非常多的逻辑,而对于DOM本身来说是非常不方便的;

其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点

  • 如渲染在canvas、WebGL、SSR、Native(iOS、Android)上;
  • 并且Vue允许你开发属于自己的渲染器(renderer),在其他的平台上渲染;

虚拟DOM的渲染过程

image-20210825220744562

三大核心系统

事实上Vue的源码包含三大核心:

  • Compiler模块:编译模板系统;
  • Runtime模块:也可以称之为Renderer模块,真正渲染的模块;
  • Reactivity模块:响应式系统;

image-20210825220828228

三大系统协同工作

三个系统之间如何协同工作呢:

image-20210825220853441

实现Mini-Vue

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

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

渲染系统,该模块主要包含三个功能:

  • 功能一:h函数,用于返回一个VNode对象;
  • 功能二:mount函数,用于将VNode挂载到DOM上;
  • 功能三:patch函数,用于对两个VNode进行对比,决定如何处理新的VNode;
h函数 – 生成VNode

h函数的实现:直接返回一个VNode对象即可

// 定义 h 函数
/**
 * 
 * @param {*} tag 元素名
 * @param {*} props 属性
 * @param {*} children 子元素
 */
const h = (tag, props, children) => {
  // vnode -> js对象 -> {}
  return {
    tag,
    props,
    children,
    // vnode -> el -> 记录一份真实dom
    // el
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
Mount函数 – 挂载VNode

mount函数的实现:

  • 第一步:根据tag,创建HTML元素,并且存储 到vnode的el中;

  • 第二步:处理props属性

    • 如果以on开头,那么监听事件;
    • 普通属性直接通过 setAttribute 添加即可;
  • 第三步:处理子节点

    • 如果是字符串节点,那么直接设置 textContent/innerHTML;
    • 如果是数组节点,那么遍历调用 mount 函 数
// 挂载功能
/**
 * 
 * @param {*} vnode 虚拟dom
 * @param {*} container 挂载的容器
 */
const mount = (vnode, container) => {
  // vnode -> element
  // 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开头 做函数的事件监听
      if (key.startsWith("on")) {
        el.addEventListener(key.slice(2).toLowerCase(), value);
      } else {
        // 设置属性
        el.setAttribute(key, value);
      }
    }
  }
  // 3. 处理children
  if (vnode.children) {
    if (typeof vnode.children === "string") {
      // 字符串
      el.innerHTML = vnode.children;
    } else if (vnode.children instanceof Array) {// 数组进行类型检测 拿到的是object
      // 数组
      vnode.children.forEach(childVnode => {
        mount(childVnode, el);
      })
    }
  }
  if (!container) {
    // 没有容器
    throw new Error('container is must be exist');
  }
  // 将el 挂载到container容器上
  else if (typeof container === "string") {
    // 是字符串,则传进来的是选择器
    document.querySelector(container).appendChild(el);
  } else {
    // 是已经选中的元素
    container.appendChild(el);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
Patch函数 – 对比两个VNode

patch函数的实现,分为两种情况:

  • n1和n2是不同类型的节点:

    • 找到n1的el父节点,删除原来的n1节点的el;
    • 挂载n2节点到n1的el父节点上;
  • n1和n2节点是相同的节点:

    • 处理props的情况
      • 先将新节点的props全部挂载到el上;
      • 判断旧节点的props是否不需要在新节点上,如果不需要,那么删除对应的属性;
    • 处理children的情况
      • 如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren;
      • 如果新节点不同一个字符串类型:
        • 旧节点是一个字符串类型
          • 将el的textContent设置为空字符串;
          • 就节点是一个字符串类型,那么直接遍历新节点,挂载到el上;
        • 旧节点也是一个数组类型
          • 取出数组的最小长度;
          • 遍历所有的节点,新节点和旧节点进行path操作;
          • 如果新节点的length更长,那么剩余的新节点进行挂载操作;
          • 如果旧节点的length更长,那么剩余的旧节点进行卸载操作;
// patch 函数
/**
 * 
 * @param {*} n1 vnode1
 * @param {*} n2 vnode2
 */
const patch = (n1, n2) => {
  // 先判断类型(元素)是否相同
  if (n1.tag !== n2.tag) {
    // 类型不同 肯定不同了
    // 移除vnode1,换成vnode2 通过父节点移除
    const parent = n1.el.parentElement;
    parent.removeChild(n1.el);
    // 创建新的vnode2节点,并挂载到父节点上
    mount(n2, parent);
  } else {
    // 类型相同
    // 1. 取出元素(element)对象,并且在n2中也进行保存el
    const el = n2.el = n1.el;
    // 2. 处理props
    // 如果props为null 则默认为空对象
    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 (oldValue !== newValue) {
        // 事件处理 on开头
        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 in newProps)) {
        // 移除事件
        if (key.startsWith("on")) {
          el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
        } else {
          // 移除属性
          el.removeAttribute(key);
        }
      }
    }

    // 3. 处理children
    const oldChildren = n1.children || [];
    const newChildren = n2.children || [];
    if (typeof newChildren === "string") {
      // 新子节点是字符串类型数据 且内容不同 直接完成内容替换
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) // 有需要的话 可以自行加更多的边界判断
          el.textContent = newChildren;
      } else {
        // oldChildren不是字符串类型
        el.innerHTML = newChildren;
      }
    }
    // 不是字符串类型 则在这里就是数组类型 对象形式我们不考虑
    else {
      if (typeof oldChildren === "string") {
        // string vs array
        el.innerHTML = "";
        // 遍历newChildren 挂载 子节点
        newChildren.forEach(child => {
          mount(child, el);
        })
      } else {
        // array vs array 都是数组
        // old: [v1,v2,v3,v5,v7]
        // new: [v3,v5,v6,v8,v9,v11]
        // diff 算法 进行比对(这里不考虑key)

        // 拿到新旧child数组较短的那个
        const commonLength = Math.min(oldChildren.length, newChildren.length);
        // 1. 遍历 将共有长度的那些节点进行patch
        for (let i = 0; i < commonLength; i++) {
          // 循环进行patch操作
          patch(oldChildren[i], newChildren[i]);
        }

        // 2. 旧child数组的长度更长,则删除多余的元素
        if (oldChildren.length > commonLength) {
          oldChildren.slice(commonLength).forEach(child => {
            // 移除原来挂载的子元素
            el.removeChild(child.el);
          })
        } else if (newChildren > commonLength) {
          // 3. 新的child的数组长度更长 则做新增操作
          // 截取数组后面多出来的部分进行遍历,挂载
          newChildren.slice(commonLength).forEach(child => {
            // 挂载
            mount(child, el);
          })
        }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
测试

代码如下:

<!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>
    const vnode = h("div", { class: 'mao', name: '111' }, [
      h("h2", {}, "你好!!!!"),
      h("h2", null, "你好!当前计数:100"),
      h("button", { onClick: function () { } }, "+1")
    ]);
    // 通过mount函数 将vnode 挂载到div#app上
    mount(vnode, "#app")

    // 创建新的vnode 这个作为更新数据后的vnode
    const vnode2 = h("div", { class: 'codermao', data: "mao" }, [
    h("h2", {}, "你好!!!!"),  
    h("h2", { style: "color:red" }, "哈哈哈")
    ]);
    // 新的vnode和旧的vnode进行diff算法的比对,找出不同
    // patch函数进行比对
    setTimeout(() => { patch(vnode, vnode2); }, 3000);
  </script>
</body>

</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
// 定义 h 函数
/**
 * 
 * @param {*} tag 元素名
 * @param {*} props 属性
 * @param {*} children 子元素
 */
const h = (tag, props, children) => {
  // vnode -> js对象 -> {}
  return {
    tag,
    props,
    children,
    // vnode -> el -> 记录一份真实dom
    // el
  }
}

// 挂载功能
/**
 * 
 * @param {*} vnode 虚拟dom
 * @param {*} container 挂载的容器
 */
const mount = (vnode, container) => {
  // vnode -> element
  // 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开头 做函数的事件监听
      if (key.startsWith("on")) {
        el.addEventListener(key.slice(2).toLowerCase(), value);
      } else {
        // 设置属性
        el.setAttribute(key, value);
      }
    }
  }
  // 3. 处理children
  if (vnode.children) {
    if (typeof vnode.children === "string") {
      // 字符串
      el.innerHTML = vnode.children;
    } else if (vnode.children instanceof Array) {// 数组进行类型检测 拿到的是object
      // 数组
      vnode.children.forEach(childVnode => {
        mount(childVnode, el);
      })
    }
  }
  if (!container) {
    // 没有容器
    throw new Error('container is must be exist');
  }
  // 将el 挂载到container容器上
  else if (typeof container === "string") {
    // 是字符串,则传进来的是选择器
    document.querySelector(container).appendChild(el);
  } else {
    // 是已经选中的元素
    container.appendChild(el);
  }
}


// patch 函数
/**
 * 
 * @param {*} n1 vnode1
 * @param {*} n2 vnode2
 */
const patch = (n1, n2) => {
  // 先判断类型(元素)是否相同
  if (n1.tag !== n2.tag) {
    // 类型不同 肯定不同了
    // 移除vnode1,换成vnode2 通过父节点移除
    const parent = n1.el.parentElement;
    parent.removeChild(n1.el);
    // 创建新的vnode2节点,并挂载到父节点上
    mount(n2, parent);
  } else {
    // 类型相同
    // 1. 取出元素(element)对象,并且在n2中也进行保存el
    const el = n2.el = n1.el;
    // 2. 处理props
    // 如果props为null 则默认为空对象
    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 (oldValue !== newValue) {
        // 事件处理 on开头
        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 in newProps)) {
        // 移除事件
        if (key.startsWith("on")) {
          el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
        } else {
          // 移除属性
          el.removeAttribute(key);
        }
      }
    }

    // 3. 处理children
    const oldChildren = n1.children || [];
    const newChildren = n2.children || [];
    if (typeof newChildren === "string") {
      // 新子节点是字符串类型数据 且内容不同 直接完成内容替换
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) // 有需要的话 可以自行加更多的边界判断
          el.textContent = newChildren;
      } else {
        // oldChildren不是字符串类型
        el.innerHTML = newChildren;
      }
    }
    // 不是字符串类型 则在这里就是数组类型 对象形式我们不考虑
    else {
      if (typeof oldChildren === "string") {
        // string vs array
        el.innerHTML = "";
        // 遍历newChildren 挂载 子节点
        newChildren.forEach(child => {
          mount(child, el);
        })
      } else {
        // array vs array 都是数组
        // old: [v1,v2,v3,v5,v7]
        // new: [v3,v5,v6,v8,v9,v11]
        // diff 算法 进行比对(这里不考虑key)

        // 拿到新旧child数组较短的那个
        const commonLength = Math.min(oldChildren.length, newChildren.length);
        // 1. 遍历 将共有长度的那些节点进行patch
        for (let i = 0; i < commonLength; i++) {
          // 循环进行patch操作
          patch(oldChildren[i], newChildren[i]);
        }

        // 2. 旧child数组的长度更长,则删除多余的元素
        if (oldChildren.length > commonLength) {
          oldChildren.slice(commonLength).forEach(child => {
            // 移除原来挂载的子元素
            el.removeChild(child.el);
          })
        } else if (newChildren > commonLength) {
          // 3. 新的child的数组长度更长 则做新增操作
          // 截取数组后面多出来的部分进行遍历,挂载
          newChildren.slice(commonLength).forEach(child => {
            // 挂载
            mount(child, el);
          })
        }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
效果

image-20210825222303539

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/751901?site
推荐阅读
相关标签
  

闽ICP备14008679号