如何实现自己的react(2)虚拟dom和render

601 阅读3分钟

前言

现在我们完成了配置,可以在js文件中开始实现我们自己的react了,首先要了解的是react是如何进行渲染的?
我的理解是:根据jsx模板生成虚拟dom -> 虚拟dom根据属性渲染为真实dom -> 将真实dom挂载到页面的dom节点下 -> 页面呈现出想要的效果

生成虚拟dom

React提供了createElement这个方法来创建虚拟dom元素,具体格式在1中提到过了,所以我们需要自己实现一个createElement方法,来返回我们需要的标签名、属性和子节点。

/**
 * createElement(tag, attrs, children)
 * @param {string} tag 代表标签名 - div/span/p等
 * @param {object} attrs 代表标签属性 - value/style等
 * @param {array} children 代表标签下的子节点
 * createElement的主要作用就是创建虚拟dom对象,所以在这里将接受到的参数返回
 */
MyReact.prototype.createElement = function(tag, attrs, ...children) {
    return {
        tag,
        attrs,
        children // children可能不止一个,所以使用了...运算符
    };
}

虚拟dom转化为真实dom

为了将虚拟dom转化成真实dom,定义了一个名为mountComponent的方法来接受虚拟dom,并返回真实dom。其中需要考虑到的问题是: 如果接受到的虚拟dom不是对象怎么办? 如果虚拟dom还有子节点怎么办?

function mountComponent(vDom) {
    /**
     * <div>{null}</div> | <div>{undefined}</div> | <div>{true}</div> | <div>{false}</div> | <div/>
     * 以上的输出结果可以尝试一下?
     */
    if (vDom === null || vDom === undefined || typeof vDom === 'boolean') {
        vDom = "";
        return vDom;
    } else if (typeof vDom === 'string' || typeof vDom === 'number') {
        // 文本节点
        vDom = vDom.toString();
        const textDom = document.createTextNode(vDom);
        return textDom;
    }
    const dom = document.createElement(vDom.tag);
    setAttributes(dom, vDom.attrs);
    // 递归调用直到最后的子节点
    vDom.children.forEach(element => {
        dom.appendChild(mountComponent(element));
    });
    return dom;
}

增加节点属性

上面将虚拟dom渲染成真实dom的过程中,我们还需要将vDom里的attrs属性赋予到生成的真实dom上,于是在mountComponent()中,去使用了setAttribute()方法

/**
 * 将attrs里的属性赋到dom上,例如
 * attrs: { style: {{ color: 'red' }}, value: 'value', onClick: fn() }
 */
function setAttributes(dom, attrs) {
    if (typeof attrs !== 'object') {
        return dom;
    } else {
        Object.keys(attrs).forEach((key) => {
            const value = attrs[key];
            if (key === 'style') {
                // 样式属性
                for (let styleName in value) {
                    dom.style[styleName] = value[styleName];
                }
            } else if (/on[A-Z]/.test(key)) {
                // 将例如onClick转换为click
                dom.addEventListener(key.slice(2).toLowerCase(), value, false);
            } else {
                dom.setAttribute(key, value);
            }
        });
    }
}

挂载到render容器

现在我们通过mountComponent()方法,最后成功得到了要放到页面内的真实dom,因为在平时的项目中,会在入口文件使用诸如React.render(<App/>, document.getElementById("root"))的代码,所以我们实现的render方法应该接受一个由虚拟dom转换而成的真实dom,和一个页面内挂载真实dom的容器container,所以我们在html中写入了一个id为root的div标签。

/**
 * render方法:将虚拟dom渲染到真实页面的过程
 * @param {object} vDom 
 * @param {*} container 
 */
MyReact.prototype.render = function(vDom, container) {
    // 清空上一次挂载的内容,准备挂载新的内容
    container.innerHTML = "";
    container && container.appendChild(mountComponent(vDom));
}

运行结果

现在我们已经完成了最基础的虚拟dom和render方法,写一段jsx语法来实验一下吧!

const React = new MyReact();

const number = 1; // 实验jsx的{}内可以使用变量

const el = (
    <div id="test">
        <p style={{ color: "red" }}>这是staru的jsx语法</p>
        <button onClick={() => { console.log('click') }}>按钮</button>
        <input value={number} onChange={(e) => { console.log(e.target.value) }} />
    </div>
);

React.render(el, document.getElementById("root"));

然后使用package.json里的脚本npm run start启动(上一篇里有具体内容)

image.png 打包成功了!让我们去html的网页里看看吧
结果是毫无疑问地报错了……

image.png 通过控制台我们可以很方便地调试代码

image.png 处理了一通代码里的bug后……运行成功,是忘记把vDom.children生成的dom节点挂载到父节点下面了,然后再次npm run start,页面出现了我们想要的内容!上面贴的代码有错误的地方也更新啦。

image.png
接下来,我将去探索如何实现react的组件、props、state和生命周期。