3.初识React之VDOM渲染

267 阅读3分钟

React的虚拟DOM转化为真实DOM

在上一篇React的虚拟DOM生成中,我们成功的将jsx转换的函数转变为了虚拟DOM,我们现在需要做的就是将其转化为真实DOM并渲染在界面上。

首先我们来过一下转化的过程

jsx

<h1 className='title' style={{ color: "red" }}>hello<span>word</span></h1>

babel翻译后

React.createElement("h1", {
  className: "title", 
  style: {
    color: "red"
  }
}, "hello", React.createElement("span", null, "word"))

React.createElement转化后

{
    "props": {
        "className": "title",
        "style": {
            "color": "red"
        },
        "children": [
            {
                "props": {
                    "content": "hello"
                }
            },
            {
                "props": {
                    "children": [
                        {
                            "props": {
                                "content": "word"
                            }
                        }
                    ]
                },
                "type": "span"
            }
        ]
    },
    "type": "h1"
}

然后我们看看react中是怎么渲染到页面上的

import React from "react
import ReactDOM from "react-dom"

const element = (<h1 className='title' style={{ color: "red" }}>hello<span>word</span></h1>)

ReactDOM.render(
  element,
  document.getElementById('root')
);

分析:

  • 通过ReactDOM这对象的render方法进行渲染
  • 第一个参数为渲染对象
  • 第二个参数为渲染的容器节点

处理:

  • 创建一个react-dom.js文件
  • 导出ReactDOM对象
  • ReactDOM中带有属性render,并提供render方法

实现ReactDOM.render方法

/**
 * 虚拟dom转真实dom
 * @param {*} vdom 虚拟dom
 * @param {*} container 容器
 */
function render(vdom, container) {
    mount(vdom, container)
}

const ReactDOM = {
    render
}

export default ReactDOM;

为了代码的复用性和可读性,我们添加一个mount方法,意为挂载,专注一个事情,将元素添加到容器中

/**
 * 挂载方法
 * @param {*} vdom 虚拟dom
 * @param {*} container 挂载容器
 */
function mount(vdom, container) {
    let newDOM = createDOM(vdom);
    if (newDOM) {
        container.appendChild(newDOM);
    }
}

我们的挂载方法依然只是简单的几行代码

  • 创建一个DOM元素,我们又添加了一个createDOM方法,专门创建元素使用
  • 如果DOM存在就添加到容器中
function createDOM(vdom) {
    if (!vdom) return null;
    let { key, props, ref, type } = vdom;
    let dom; // 真实dom
    // 如果是文本类型,创建一个文本节点
    if (type === REACT_TEXT) {
        dom = document.createTextNode(props.content);
    } else {
        dom = document.createElement(type);
    }
    if (props) {
        updateProps(dom, {}, props);
        if(props.children){
            let children = props.children;
            if(Array.isArray(children)){
                reconcileChildren(children, dom)
            }
        }
    } 
    return dom;
}
  • 判断是否存在虚拟DOM,不存在就返回null

  • 判断DOM节点是否为文本节点,如果是文本节点,就创建文本节点

  • 如果不是文本节点,那么就创建一个type的节点【type保存着节点的标签名】

  • 判断props是否存在,如果存在,那么就进行修改props,这里的话,我们还是创建了一个【updateProps方法】专门处理该事情

    • 参数一: 当前DOM,后面属性是不是要添加到当前DOM上
    • 参数二:空对象,这个表示一个旧属性,后面diff的时候需要,我们提前预留下,因为第一次没有,所以是空对象
    • 参数三:props,这个就表示当前的一个新的属性对象
  • 判断一下是否有children属性,如果children属性存在,并且是个数组的话,那么就使用reconcileChildren方法对子节点进行一个挂载

    • 这里处理children属性是因为children也是属于DOM的,并非属性的,所以放在当前方法处理更为合适,当然放在props属性中处理也不是不行
  • 返回DOM元素

我们先看那个updateProps方法

/**
 * 更新props,后面可能进行diff,所以带有旧属性
 * @param {*} dom 真实dom
 * @param {*} oldProps 旧属性
 * @param {*} newProps 新属性
 */
function updateProps(dom, oldProps, newProps) {
    for (const key in newProps) {
        // 当前值
        const data = newProps[key];
        // 子属性忽略
        if (key === "children") { continue; }
        // 样式类型单独处理
        else if (key === "style") {
            for (const attr in data) {
                dom.style[attr] = data[attr];
            }
        }
        // 其他属性直接赋值
        else {  
            dom[key] = data;
        }
    } 
}
  • 循环取出所有的属性值与key
  • 判断key是否为children,因为我们在createDOM方法中处理了该属性,所以跳过
  • 判断key是否为stylestyle这个是一个对象,并且我们需要特殊对待他,所以单独处理一下
  • 其他情况下直接赋值,如果还有什么需要特殊处理的可以在这里进行判断

最后我们再看看reconcileChildren方法

/**
 * 递归处理子元素
 * @param {*} childrenVdom  子元素
 * @param {*} parentDOM  父元素
 */
function reconcileChildren(childrenVdom, parentDOM){
    childrenVdom.forEach(children => mount(children, parentDOM));
}

我们直接挂载就好了,这样形成递归,渲染所有的子节点

最后我们可以看看效果了

import React from './package/react';
import ReactDOM from './package/react-dom';
 
  
const element = React.createElement("h1", {
  className: "title", 
  style: {
    color: "red"
  }
}, "hello", React.createElement("span", null, "word"))
 

ReactDOM.render(
  element,
  document.getElementById('root')
);