一分钟早读系列:渲染器(render)

提到前端框架,那就不得不提到两个重要的概念:渲染器。本文将Vue为例,简单介绍下渲染器的作用。

在了解这两个是什么之前,我们要先理解一个概念,那就是......

虚拟DOM

所谓虚拟DOM就是用JS对象来描述真实DOM的一种数据结构。举个例子

js 复制代码
const title = {
    // 标签名字
    tag: "h1",
    // 标签属性
    props: {
        onClick: handler
    },
    // 子列表
    children: [
        { tag: "span" }
    ]
}

对应到模板中就是

js 复制代码
<h1 @click="handler"><span></span></h1>

如果还有子节点,那么需要编写的js对象就更长了。那么,虚拟DOM是如何变成真实DOM并渲染到页面中的呢?

渲染器

渲染器的作用,就是将这种JS对象转换成真实DOM。我们平时开发Vue组件都是依赖渲染器工作的。

假设我们有如下虚拟DOM

js 复制代码
const vnode = {
    tag: 'div',
    props: {
        onClick: () => alert('hello'),
        style: 'width:100px;height:100px;font-size:24px;'
    },
    children: 'click'
}

为了将vnode正确转换成真实DOM,我们需要编写一个renderer函数

js 复制代码
function renderer(vnode, container) {
    // 使用tag创建DOM元素
    const el = document.createElement(vnode.tag)
    // 遍历props,将属性和时间挂载到DOM元素上
    for (const key in vnode.props) {
        if (/^on/.test(key)) {
            // 如果是以on开头,代表是事件,这里要进行截取,将onCLick转成click
            el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key])
        } else {
            // 其余的认定为属性
            el.setAttribute(key, vnode.props[key])
        }
    }
    if (typeof vnode.children === 'string') {
        // 如果children是字符串,说明它是文本内容
        el.appendChild(document.createTextNode(vnode.children))
    } else if (Array.isArray(vnode.children)) {
        // 递归调用renderder方法,循环渲染子节点,挂载到生成的DOM元素上
        vnode.children.forEach(child => renderer(child, el))
    }
    // 将渲染出来的节点挂载到指定节点
    container.appendChild(el)
}

这里的renderer有两个函数

  1. vnode:虚拟DOM对象
  2. container:一个真实DOM节点,作为挂载对象传入,渲染器会把渲染好的节点挂载到这里

接下来只要调用renderer函数就可以了

js 复制代码
renderer(vnode, document.body)

就可以在浏览器中看到真实节点了

回过头分析一下渲染器的流程,分三个步骤

  1. 根据tag创建DOM元素
  2. 将props上的方法和属性挂载到DOM元素上
  3. 循环递归处理children,将处理好的child挂载到DOM元素上

当然,这只是简易的示意代码,在Vue中则更为复杂,比如目前我们每次执行renderer都是全量渲染,但如果我们只将children的值'click'改成'click again'呢?这个时候需要渲染器进行更精确的渲染,也就是我们比较熟悉的diff流程。当然更详细的不在这里多讲,后续会专门开一篇去讲,这里只是给大家讲解一下概念。

现在,我们还有个问题,那就是我们平时开发Vue组件的时候基本是很少见到虚拟DOM这种结构的,大多数时候都是组件的形态,那么组件是什么?组件和渲染器之间的关系又是什么呢?

组件的本质

其实组件就是一组DOM元素的封装。一个组件可以是对象,也可以是对象,最终输出的都是虚拟DOM,举个例子

js 复制代码
const myComponent = function () {
    return {
        tag: 'div',
        props: {
            onClick: () => alert('hello'),
            style: 'width:100px;height:100px;font-size:24px;'
        },
        children: 'click'
    }
}

可以看到,最终返回的还是虚拟DOM。实际上,对于虚拟DOM来讲,组件也可以是一个tag,比如

js 复制代码
const vnode = {
    tag: myComponent
}

同理,组件引用的方式也可以变换成

js 复制代码
const vnode = {
    tag: 'div',
    children: [myComponent]
}

那么为了支持这种组件tag,我们需要对renderer进行一定程度的改造,改造起来也非常简单,让其支持函数就可以了

js 复制代码
function renderer(vnode, container) {
    if (typeof vnode.tag === 'string') {
        // 说明是正常的标签
        mountElement(vnoode, container)
    } else if (typeof vnode.tag === 'function') {
        // 说明是组件
        mountComponent(vnode, container)
    }
}

正常的标签还是走原来的逻辑就可以了

js 复制代码
function mountElement(vnode, container) {
    // 使用tag创建DOM元素
    const el = document.createElement(vnode.tag)
    // 遍历props,将属性和时间挂载到DOM元素上
    for (const key in vnode.props) {
        if (/^on/.test(key)) {
            // 如果是以on开头,代表是事件,这里要进行截取,将onCLick转成click
            el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key])
        } else {
            // 其余的认定为属性
            el.setAttribute(key, vnode.props[key])
        }
    }
    if (typeof vnode.children === 'string') {
        // 如果children是字符串,说明它是文本内容
        el.appendChild(document.createTextNode(vnode.children))
    } else if (Array.isArray(vnode.children)) {
        // 递归调用renderder方法,循环渲染子节点,挂载到生成的DOM元素上
        vnode.children.forEach(child => renderer(child, el))
    }
    // 将渲染出来的节点挂载到指定节点
    container.appendChild(el)
}

组件也非常简单

js 复制代码
function mountComponent(vnode, container) {
    // 执行函数,获取虚拟DOM
    cosnt subtree = vnode.tag()
    // 重新走一遍渲染即可
    renderer(suvtree, container)
}

上述代码只演示了函数形态,如果是对象则需要简单变通一下即可

js 复制代码
const myComponent = {
    render: function () {
        return {
            tag: 'div',
            props: {
                onClick: () => alert('hello'),
                style: 'width:100px;height:100px;font-size:24px;'
            },
            children: 'click'
        }
    }
}

function renderer(vnode, container) {
    if (typeof vnode.tag === 'string') {
        // 说明是正常的标签
        mountElement(vnoode, container)
    } else if (typeof vnode.tag === 'object') {
        // 说明是组件
        mountComponent(vnode, container)
    }
}

function mountComponent(vnode, container) {
    // 执行函数,获取虚拟DOM
    cosnt subtree = vnode.tag.render()
    // 重新走一遍渲染即可
    renderer(suvtree, container)
}

有这么几处改动

  1. 将组件形态变更为对象形态,内部包含一个render函数。
  2. 判断条件由function改为object
  3. 不再直接执行tag,而是执行tag.render

如此一来,我们就可以正确的渲染一个组件了

总结

本文介绍了何为虚拟DOM,渲染器的基本概念,以及组件是什么。虚拟DOM本质上是描述真实DOM的一种JS对象结构,组件本身也是一种描述真实DOM的结构,解析组件之后最终返回的是虚拟DOM。而渲染器是一种将虚拟DOM转为真实DOM的工具。

相关推荐
凹凸曼打不赢小怪兽18 分钟前
react 受控组件和非受控组件
前端·javascript·react.js
狂奔solar28 分钟前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes
qiyi.sky31 分钟前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
清云随笔1 小时前
axios 实现 无感刷新方案
前端
鑫宝Code1 小时前
【React】状态管理之Redux
前端·react.js·前端框架
忠实米线1 小时前
使用pdf-lib.js实现pdf添加自定义水印功能
前端·javascript·pdf
pink大呲花1 小时前
关于番外篇-CSS3新增特性
前端·css·css3
少年维持着烦恼.1 小时前
第八章习题
前端·css·html
我是哈哈hh1 小时前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
田本初1 小时前
如何修改npm包
前端·npm·node.js