渲染器 render

最近在看《vue.js 设计与实现》,看到了虚拟 DOM 这里,做了个笔记

1、VNode虚拟DOM节点

  • 虚拟DOM节点如下:

    const vnode = {
      tag: 'h2',
      props: {
        class: 'active',
        data: 'text',
        onClick: () => alert('Hello Render!')
      },
      children: 'Click Me!'
    }
    
    1. tag:用来描述标签名称,所以tag: 'h2'描述的就是一个<h2>标签。
    2. props:是一个对象,用来描述<h2>标签的类名、属性、事件等内容。可以看到,我给h2绑定一个active类名,一个text自定义属性,一个click点击事件。
    3. children:用来描述标签的子节点,在上面的代码中,children是一个字符串值,意思是h2标签有一个文本子节点:<h2>Click Me!</h2>

2、render工作原理

  1. 第一步: 创建元素,把vnode.tag作为标签名称来创建DOM元素。
  2. 第二步: 为元素添加属性和事件,遍历vnode.props对象。如果key是class,说明它是一个类名,将其直接绑定给tag元素;如果key以on字符串开头,说明它是一个事件,把字符on截取掉后再调用toLowerCase函数将事件名称小写化,最终得到合法的事件名称,例如onClick会变成click,最后调用addEventListener绑定事件处理函数。
  3. 第三步: 处理children,如果children是一个数组,就递归地调用render继续渲染。注意,此时我们要把刚刚创建的元素作为挂载节点(父节点);如果children是字符串,则使用createTextNode函数创建一个文本节点,并将其添加到新的创建的元素内。

3、代码实现

  • 自己手写一个简陋的render渲染器,上代码:

    <!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>
      <style>
        .active {
          color: red;
        }
      </style>
    </head>
    
    <body>
      <!-- 根节点 -->
      <div id="app"></div>
    
      <script>
        // 获取根节点
        const app = document.getElementById("app");
    
        // 要进行的挂载的虚拟 dom 节点
        const vnode = {
          // 节点类型
          tag: 'h2',
          // 节点上面绑定的类型、属性、方法
          props: {
            class: 'active', // 类名
            data: 'text', // 自定义属性
            onClick: () => alert('Hello Render!') // 方法
          },
          // 子节点
          children: 'Click Me!'
        }
    
        // 封装的渲染器 render 函数
        // vnode 是虚拟 dom 节点
        // container 是要挂载节点的元素
        const render = (vnode, container) => {
          // 根据 vnode.tag 创建对应的 dom 节点
          const el = document.createElement(vnode.tag);
    
          // 遍历 vnode.props,给节点绑定类名、属性、事件等
          for (const key in vnode.props) {
            if (key === 'class') {
              // 如果是类名,则给元素添加类名
              el.className += vnode.props[key]
            } else if (/^on/.test(key)) {
              // 如果属性是以 on 开头的,那么就绑定对应的事件
              el.addEventListener(
                key.substr(2).toLowerCase(), // 改变事件类型:例如 onClick 变为 click
                vnode.props[key] // 绑定对应的事件
              )
            } else {
              // 如果是自定义属性,给元素绑定对应的属性
              el.setAttribute(key, vnode.props[key])
            }
          }
    
          // 处理子节点
          if (typeof vnode.children === 'string') {
            // 如果子节点是 string 类型,那么就是本文节点
            el.appendChild(document.createTextNode(vnode.children));
          } else if (Array.isArray(vnode.children)) {
            // 如果子节点是 array 类型,那么继续渲染子节点
            vnode.children.forEach(child => render(child, el));
          }
    
          // 将渲染的 vnode 挂载在根节点上
          app.appendChild(el);
        }
    
        // 调用 render 函数
        render(vnode, app)
      </script>
    </body>
    
    </html>
    
  • 看一下页面效果:

    在这里插入图片描述

4、render拓展

在上面进行VNode渲染的过程,我只是讨论了当VNode是一个虚拟DOM节点的情况,那么接下来我将会讨论组件component的渲染,而component组件应该有两种情况

  1. component是一个函数;
  2. component是一个对象;
  1. 首先直接来看代码的实现:

    <!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>
        <style>
          .active1 {
            color: red;
          }
    
          .active2 {
            color: blue;
          }
    
          .active3 {
            color: orange;
          }
        </style>
      </head>
    
      <body>
        <!-- 根节点 -->
        <div id="app"></div>
    
        <script>
          // 获取根节点
          const app = document.getElementById("app");
    
          // 要进行绑定的 component 组件 —— 返回函数
          const component1 = function () {
            return {
              // 节点类型
              tag: 'h2',
              // 节点上面绑定的类型、属性、方法
              props: {
                class: 'active2', // 类名
                data: 'component1', // 自定义属性
                onClick: () => alert('Hello Component1!') // 方法
              },
              // 子节点
              children: 'Function Component!'
            }
          }
    
          // 要进行绑定的 component 组件 —— 返回对象
          const component2 = {
            // 该组件对象的 render 属性
            render() {
              return {
                // 节点类型
                tag: 'h2',
                // 节点上面绑定的类型、属性、方法
                props: {
                  class: 'active3', // 类名
                  data: 'component2', // 自定义属性
                  onClick: () => alert('Hello Component2!') // 方法
                },
                // 子节点
                children: 'Object Component!'
              }
            }
          }
    
          // 要进行的挂载的虚拟 dom 节点(标签元素)
          const vnode1 = {
            // 节点类型
            tag: 'h2',
            // 节点上面绑定的类型、属性、方法
            props: {
              class: 'active1', // 类名
              data: 'vnode', // 自定义属性
              onClick: () => alert('Hello Render!') // 方法
            },
            // 子节点
            children: 'Click Me!'
          }
    
          // 要进行的挂载的虚拟 dom 节点(组件1)
          const vnode2 = {
            tag: component1
          }
    
          // 要进行的挂载的虚拟 dom 节点(组件2)
          const vnode3 = {
            tag: component2
          }
    
          // 挂载标签元素的方法
          const momentElement = (vnode, container) => {
            // 根据 vnode.tag 创建对应的 dom 节点
            const el = document.createElement(vnode.tag)
    
            // 遍历 vnode.props,给节点绑定类名、属性、事件等
            for (const key in vnode.props) {
              if (key === 'class') {
                // 如果是类名,则给元素添加类名
                el.className += vnode.props[key]
              } else if (/^on/.test(key)) {
                // 如果属性是以 on 开头的,那么就绑定对应的事件
                el.addEventListener(
                  key.substr(2).toLowerCase(), // 改变事件类型:例如 onClick 变为 click
                  vnode.props[key] // 绑定对应的事件
                )
              } else {
                // 如果是自定义属性,给元素绑定对应的属性
                el.setAttribute(key, vnode.props[key])
              }
            }
    
            // 处理子节点
            if (typeof vnode.children === 'string') {
              // 如果子节点是 string 类型,那么就是本文节点
              el.appendChild(document.createTextNode(vnode.children))
            } else if (Array.isArray(vnode.children)) {
              // 如果子节点是 array 类型,那么继续渲染子节点
              vnode.children.forEach(child => render(child, el))
            }
    
            // 将渲染的 vnode 挂载在根节点上
            app.appendChild(el);
          }
    
          // 挂载组件的函数
          const momentComponent = (vnode, container) => {
            // 如果组件返回的是函数:调用组件函数,获取组件要渲染的内容(虚拟 DOM)
            // 如果组件返回的是对象:vnode.tag 是组件对象,调用它的 render 函数得到组件要渲染的内容(虚拟 DOM)
            const componentTree = typeof vnode.tag === 'function' ? vnode.tag() : vnode.tag.render()
    
            // 递归地调用 render 渲染 componentTree
            render(componentTree, container)
          }
    
          // 封装的渲染器 render 函数
          // vnode 是虚拟 dom 节点
          // container 是要挂载节点的元素
          const render = (vnode, container) => {
            if (typeof vnode.tag === 'string') {
              // 如果 vnode.tag 是 string 类型,则表示该 vnode 是标签元素,调用挂载标签元素的函数
              momentElement(vnode, container)
            } else if (typeof vnode.tag === 'object' || typeof vnode.tag === 'function') {
              // 如果 vnode.tag 是 object 类型,则表示该 vnode 是组件,调用挂载组件的函数
              momentComponent(vnode, container)
            }
          }
    
          // 调用 render 函数
          render(vnode1, app)
          render(vnode2, app)
          render(vnode3, app)
        </script>
      </body>
    
    </html>
    
  2. 看一下页面效果:

    在这里插入图片描述

  3. 事项说明:

    1. 组件就是一组DOM元素的封装
    2. 代码中我的注释写的都挺清楚了,一定要注意component组件的类型分为Object和Function两种类型,这两种不同情况的返回值都是不一样的,一定要注意返回值,不要搞混。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凉爽爽爽爽爽爽爽爽爽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值