1

一、简介

前段时间看到一个用33行代码就实现了一个非常基本的react代码。感觉还是蛮有趣的,代码如下:
屏幕快照 2020-06-01 下午9.06.46.png

其主要实现了两大功能:
生成虚拟DOM
根据虚拟DOM渲染出真实的DOM

无注释版:https://github.com/leontrolsk...
有注释版:https://github.com/leontrolsk...

二、代码分析

虽然代码总共才33行,但是写的非常简洁,可能不是一下就能看懂,我对此细细研读了一番,用更加明了的方式重新写了一下。主要就是对外暴露了React.createElement()React.render()两个方法。
① 实现代码基本框架

// 定义一个React类并对外暴露
class React {
    // 负责创建虚拟DOM
    static createElement(...args) {
    
    }
    // 负责将虚拟DOM渲染成真实的DOM
    static render(parent, v) {
        
    }
}
export default React;

② 实现React.createElement()方法
createElement()方法主要就是解析传递过来的参数,然后解析出标签名类名数组属性对象子节点数组

  • 解析标签名: 第一个参数为字符串中包含标签名,这个参数支持点的形式,如 div.redColor.bluebg,所以需要以点号进行分割成数组["div", "redColor", "bluebg"],将数组的第一个元素作为标签名剩余的元素都作为元素的类名
  • 解析元素的属性对象: 通常传入的第二个参数对象为元素的属性对象,但是也可以不传直接传入元素的子节点,所以需要对第二个参数进行判断,看一下是否是属性对象。判断的依据就是,如果第二个参数是null、string、number、array、vnode,那么就不是属性对象,而是子节点。
class React {
    // 判断能不能渲染,不能渲染则为属性对象
    static isRenderable(v) {
        return v === null || ['string', 'number'].includes(typeof v) || v.__m || Array.isArray(v);
    }
}
  • 解析子节点: 解析完标签名和属性对象后,剩下的参数都是子节点,只不过参数有可能是一个数组,所以需要对其进行判断,如果是数组则需要遍历数组,然后再递归将其添加到children数组中
class React {
    // 负责创建虚拟DOM
    static createElement(...args) {
        // ① 将attrs初始化为一个空对象{},并解构出第一个参数head
        let attrs = {};
        let [head, ...tail] = args;
        // ② 获取标签名和第一个参数中传递的类名
        let [tag, ...classes] = head.split('.');
        // ③ 获取传入的attrs对象,首先判断tail中的第一部分能不能渲染,如果不能渲染,说明是attrs对象,否则为子节点
        if (tail.length && !this.isRenderable(tail[0])) { // 如果传入的第二个参数不能被渲染,那么第二个参数就是attrs对象
            [attrs, ...tail] = tail; // 取出attrs对象
        }
        // ④ 解析attrs对象中包含的类名
        if (attrs.class) { // 如果attrs属性对象中有class属性,那么将其中的类名放到classes中
            classes = [...classes, ...attrs.class];
        }
        // ⑤ 移除attrs对象中的class属性
        attrs = {...attrs}; 
        delete attrs.class;
        // ⑥ 初始化children为空数组[]
        const children = [];
        // ⑦ 定义一个addChildren()方法,将所有子节点加入到children数组中
        const addChildren = (v) => {
            if (v === null) { // 如果是null则直接返回,不将其加入到children数组中
                return;
            }
            if (Array.isArray(v)) { // 如果是数组则遍历数组并调用addChildren()
                v.map(addChildren);
            } else {
                children.push(v); // 非数组则直接将其添加到children数组中
            }
        }
        addChildren(tail);
        // ⑧ 返回虚拟DOM对象
        return {
            __m: true, // 是否是虚拟节点
            tag: tag || 'div', 
            attrs, classes, 
            children
        };
    }
}

③ 实现React.render()方法
React.render()方法主要传入一个真实的父节点和对应的虚拟节点,将真实父节点当做旧节点将虚拟节点作为新节点,然后对新旧节点进行比较,没有则创建,有则更新,同时更新属性,然后进行递归比较其子节点直到没有子节点为止

class React {
    // 进行新旧节点的比较并渲染出真实DOM
    static render(parent, v) {
        // ① 取出真实节点下的所有真实子节点
        const olds = parent.childNodes || [];
        // ② 取出虚拟节点下的children属性对应的虚拟子节点
        const news = v.children || [];
        // ③ 移除超出的元素(如果旧的节点比新的节点多)
        for (const _ of Array(Math.max(0, olds.length - news.length))) {
            parent.removeChild(parent.lastChild);
        }
        // ④ 遍历所有虚拟子节点,没有子节点了则会结束render
        news.length > 0 && news.forEach((child, i) => {
            // 如果旧子节点不存在则创建一个
            let el = olds[i] || this.makeEl(child);
            if (!olds[i]) { // 如果旧节点不存在,则直接加入
                parent.appendChild(el);
            }
            // 判断新旧节点是否匹配
            const mismatch = (el.tagName || '') !== (child.tag || '').toUpperCase();
            if (mismatch) { // 如果不匹配,则创建出新节点,并且用新节点去替换掉旧节点
                (el = this.makeEl(child)) && parent.replaceChild(el, olds[i]);
            }
            // 更新节点属性
            this.update(el, child);
            // 递归更新子节点
            this.render(el, child);
        })
    }
    static makeEl(vnode) {
        if (vnode.__m) { // 如果是元素节点
            return document.createElement(vnode.tag);
        } else { // 如果是文本节点
            return document.createTextNode(vnode);
        }
    }
}

④ 实现update()方法
主要就是传入真实节点和虚拟节点,然后遍历虚拟节点的classes类名数组,将新的类名添加到真实节点上,遍历真实节点上的所有classList,然后移除虚拟DOM上不再使用的类名。遍历虚拟DOM节点上的attrs属性对象,并将新的属性添加到真实节点上,遍历真实DOM节点上的attributes,移除虚拟DOM上不再使用的属性

class React {
    static update(el, v) {
        if (!v.__m) { // 如果是文本节点
            return el.data === `${v}` || (el.data = v);
        }
        // 遍历虚拟节点上所有的class,并添加到classList中
        for (const name of v.classes) {
            if (!el.classList.contains(name)) {
                el.classList.add(name);
            }
        }
        // 遍历真实节点的classList,移除虚拟节点没有的class
        for (const name of el.classList) {
            if (!v.classes.includes(name)) {
                el.classList.remove(name);
            }
        }
        // 遍历虚拟节点的attrs属性对象
        for (const name of Object.keys(v.attrs)) {
            if (el[name] !== v.attrs[name]) { // 如果发生变化,则更新
                el[name] = v.attrs[name]; // 更新
            }
        }
        // 遍历真实节点的attributes,移除虚拟节点上不存在的attrs
        for (const {name} of el.attributes) {
            if (!Object.keys(v.attrs).includes(name) && name !== 'class') {
                el.removeAttribute(name);
            }
        }
    }
}

JS_Even_JS
2.6k 声望3.7k 粉丝

前端工程师