乐趣区

33行react简要分析

一、简介

前段时间看到一个用 33 行代码 就实现了一个非常基本的 react 代码。感觉还是蛮有趣的,代码如下:

其主要实现了两大功能:
生成虚拟 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);
            }
        }
    }
}
退出移动版