buxuku / react-handwritten

手写实现React
0 stars 1 forks source link

2.虚拟DOM渲染成真实DOM #2

Open buxuku opened 3 years ago

buxuku commented 3 years ago

在上步我们已经生成了虚拟DOM,接下来我们就要将这个虚拟DOM渲染到真实的DOM节点上面去,这个过程就是我们调用的ReactDOM.render这个函数.

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

它接收一个虚拟DOM,以及一个真实的DOM节点,最终将这个虚拟DOM挂载到这个真实的DOM节点上面.

节点的渲染

对于这个虚拟DOM,我们知道它可能会是一个文本节点,也可能是一个HTML节点,以及我们常用的函数组件和类组件类型.所以对于虚拟DOM的处理我们要分几种情况来考虑.

当它不是一个React element时

它不是一个react element,即它不是通过React.createElement创建出来的一个对象,它是一个简单的值,也就是说当上面render函数里面的element是值是一个string,number,null,undefined这些值的时候,它也是可以正常不渲染的,所以我们先来处理这种情况.它的真实DOM就是一个普通的文本节点.

新建一个ReactDOM/index文件,我们创建一个render函数来挂载真实DOM,以及一个createDOM函数来生成真实的DOM节点.

/**
 * 将虚拟DOM渲染到真实DOM节点里面
 * @param vdom
 * @param container
 */
function render(vdom, container){
    if(vdom === null || vdom === undefined) return; // 如果vdom不存在,则不需要创建真实dom;
    const dom = createDom(vdom);
    container.appendChild(dom);
}

/**
 * 将虚拟DOM转换成真实DOM
 * @param vdom
 */
function createDom(vdom){
    let dom;
    if(typeof vdom !== 'object') { // render可以直接渲染一个字符串或者数字,它不是一个React.element
        dom = document.createTextNode(vdom);
        return dom;
    }
}

const ReactDOM = {
    render,
}

export default ReactDOM;

修改src/index文件,确实它可以正常渲染

import React from './react';
import ReactDOM from './react-dom';

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

普通的HTML元素

当虚拟DOM是一个普通的HTML元素时,我们知道它有一个type属性来表示它的类型,比如h1,div,p这些.这些我们就调用原生的Document.createElement来创建真实DOM节点.对于createDom函数增加一个类型判断

function createDom(vdom) {
    let dom;
    if (typeof vdom !== 'object') { // render可以直接渲染一个字符串或者数字,它不是一个React.element
        dom = document.createTextNode(vdom);
        return dom;
    }
+   const {type, props} = vdom;
+   if (typeof type === 'string') {
+       dom = document.createElement(type);
+   }
+   if (props) {
+       const {children} = props;
+       if (Array.isArray(children)) {
+           reconcileChildren(children, dom);
+       } else {
+           render(children, dom);
+       }
+   }
+   return dom;
}

/**
 * 依次渲染子元素
 * @param childrenVdom
 * @param parentDOM
 */
+function reconcileChildren(childrenVdom, parentDOM) {
+    for (let i = 0; i < childrenVdom.length; i++) {
+        let childVdom = childrenVdom[i];
+        render(childVdom, parentDOM);
+    }
+}

来试试渲染我们最初的hello world!看看.

import React from './react';
import ReactDOM from './react-dom';

const element = <h1>hello <span>world!</span></h1>;

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

函数式组件

对于一个函数式组件

import React from './react';
import ReactDOM from './react-dom';

function Hello({name}){
    return <h1>hello <span>{name}</span></h1>
}
console.log('element', <Hello name="world" />);

ReactDOM.render(
    <Hello name='world' />, document.getElementById('root')
);

我们可以看到,打印出来的type是一个function,并且在props上面包含了我们传递的参数

HoJNZ1

我们知道,在函数组件里面,我们最终要return一个组件回来的,所以处理函数式组件也就也就很简单了,我们执行这个函数式组件,去获取到它的返回结果,就获取到了我们要渲染的虚拟DOM.

于是,继续扩展createDom这个函数

     if (typeof type === 'string') {
         dom = document.createElement(type);
     }
+    if (typeof type === 'function'){
+        // 让type执行,返回虚拟DOM,继续处理返回的虚拟DOM
+        return createDom(type(props));
+    }
     if (props) {

我们的函数组件也可以正常渲染出来了.

对于类组件

我们先看一下原版的类组件

import React from 'react';
import ReactDOM from 'react-dom';

class Hello extends React.Component{
    render(){
        return <h1>hello <span>{this.props.name}</span></h1>
    }
}

console.log('element', <Hello name="world" />);

ReactDOM.render(
    <Hello name='world' />, document.getElementById('root')
);

看一下它创建出来的虚拟长成什么样子

LfgXIu

首先,它的type就是我们的class,并且类组件是继承于React.Component的,顺着原型链,我们可以找到一个isReactComponent这个属性,它用来标识这是一个React组件.所以,首先我们要实际一个Component这样一个父类.

新建src/react/Component.js

/**
 * React.Component父类
 */
export class Component{
    static isReactComponent = true
    constructor(props){
        this.props = props;
    }
}

并在src/react/index.js中导入导出

+import { Component } from "./Component";

const React = {
    createElement,
+   Component,
}

export default React;

和函数组件返回一个虚拟DOM一样,在类组件里面,我们始终会调用render这个函数来返回一个虚拟DOM,所以,我们同样执行这个类组件来返回虚拟DOM即可.

继续扩展createDom方法

if (typeof type === 'function') {
+    if (type.isReactComponent) { // 是一个类组件
+        return mountClassComponent(vdom);
+    }
    // 让type执行,返回虚拟DOM,继续处理返回的虚拟DOM
    return createDom(type(props));
 }

+/**
+ * 获取类组件的虚拟DOM
+ * @param vdom
+ * @returns {Text|*|HTMLElement}
+ */
+function mountClassComponent(vdom) {
+    const {type, props} = vdom;
+    const classInstance = new type(props);
+    const classInstanceVdom = classInstance.render();
+    return createDom(classInstanceVdom);
+}

现在,我们的类组件也可以正常渲染出来了.

HTML元素attribute属性的处理

目前,我们只是把节点渲染出来了,但对于标签上面的属性还没有进行任何处理,比如要渲染一个组件

import React from './react';
import ReactDOM from './react-dom';

class Hello extends React.Component {
    render() {
        return <h1 id="title" className='title'>hello <span style={{color: 'red'}}>{this.props.name}</span></h1>
    }
}

ReactDOM.render(
    <Hello name='world'/>, document.getElementById('root')
);

我们是没有处理上面的id,className,style这些属性的.其实这些属性我们也是调用js原生的方法来添加上去,只是对于style要特殊一些,因为它传入的是一个对象,而我们用js赋值的时候使用的是dom.style.key=value这样的形式.

createDom里面增加一句

    if (typeof type === 'string') {
        dom = document.createElement(type);
+        renderAttributes(dom, props);
    }

并且来实现这个方法

/**
 * 为dom节点创建attributes属性
 * @param dom
 * @param attributes
 */
function renderAttributes(dom, attributes = {}){
    for(let key in attributes){
        const value = attributes[key];
        if(!value || key === 'children') continue; // 属性无值不处理,children也单独处理
        if(key === 'style'){
            for(let attr in value){
                dom.style[attr] = value[attr];
            }
        } else {
            dom[key] = value;
        }
    }
    return dom;
}

public/index.html增加一段style样式

    <style>
        .title{
            color: green;
        }
    </style>

查看一下效果,我们的id,class,style都已经生效了.

rHGcAr