Skip to content

精读《用160行js代码实现一个React》 #13

Description

@z2014

现在网上有很多react原理解析这样的文章,但是往往这样的文章我看完过后却没有什么收获,因为行文思路太快,大部分就是写了几句话简单介绍下这段代码是用来干嘛的,然后就贴上源码让你自己看,有可能作者本人是真的看懂了,但是对于大部分阅读这篇文章的人来说,确是云里雾里。

讲解一个框架的源码,最好的方式就是实现一个简易版的,这样在你实现的过程中,读者就能了解到你整体的思路,也就能站在更高的层面上对框架有一个整体的认知,而不是陷在一些具体的技术细节上。

这篇文章就非常棒的实现了一个简单的react框架,接下来属于对原文的翻译加上一些自己在使用过程中的理解。

首先先整体介绍通过这篇文章你能学到什么--我们将实现一个简单的React,包括简单的组件级api和虚拟dom,文章也将分为以下四个部分

  • Elements:在这一章我们将学习JSX是如何被处理成虚拟DOM的
  • Rendering: 在这一小节我们将想你展示如何将虚拟dom变成真实的DOM的
  • Patching: 在这一章我们将向你展示为什么key如此重要,并且如何利用虚拟DOM对已存在的DOM进行批量更新
  • Components :最后一小节将告诉你React组件和他的生命周期

Element

元素携带者很多重要的信息,比如节点的type,props,children list,根据这些属性,能渲染出我们需要的元素,它的树形结构如下

{
    "type": "ul",
    "props": {
        "className": "some-list"
    },
    "children": [
        {
            "type": "li",
            "props": {
                "className": "some-list__item"
            },
            "children": [
                "One"
            ]
        },
        {
            "type": "li",
            "props": {
                "className": "some-list__item"
            },
            "children": [
                "Two"
            ]
        }
    ]
}

但是如果我们日常写代码如果要写成这个样子,那我们应该要疯了,所以一般我们会写jsx的语法

/** @jsx createElement */
const list = <ul className="some-list">
    <li className="some-list__item">One</li>
    <li className="some-list__item">Two</li>
</ul>;

为了能够让他被编译成常规的方法,我们需要加上注释来定义用哪个函数,最终定义的函数被执行,最后会返回给一个虚拟DOM

const createElement = (type, props, ...children) => {
    props = props != null ? props : {};
    return {type, props, children};
};

我为什么这个地方要加注释呢,因为我在用babel打包jsx的语法的时候,貌似默认用的React里提供的CreateElement,所以当时我配置了.babelrc以后
发现它报了一个React is not defined错误,但是我安装的是作者这个简易的类React包,后来才知道在jsx前要加一段注释来告诉babel编译的时候用哪个函数

/** @jsx Gooact.createElement */

Rendering

这一节是将vdom渲染真实dom

上一节我们已经得到了根据jsx语法得出的虚拟dom树形结构,那么就该将这个虚拟dom结构渲染成真实dom

那么我们在拿到一个树形结构的时候,如何判断这个节点应该渲染成真实dom的什么样子呢,这里就会有3种情况,第一种就是直接会返回一个字符串,那我们就直接生成一个文本节点,如果返回的是一个我们自定义的组件,那么我们就在调用这个方法,如果是一个常规的dom组件,我们就创建这样的一个dom元素,然后接着继续遍历它的子节点。

setAttribute就是将我们设置在虚拟dom上的属性设置在真实dom上

const render = (vdom, parent=null) => {
    if (parent) parent.textContent = '';
    const mount = parent ? (el => parent.appendChild(el)) : (el => el);
    if (typeof vdom == 'string' || typeof vdom == 'number') {
        return mount(document.createTextNode(vdom));
    } else if (typeof vdom == 'boolean' || vdom === null) {
        return mount(document.createTextNode(''));
    } else if (typeof vdom == 'object' && typeof vdom.type == 'function') {
        return mount(Component.render(vdom));
    } else if (typeof vdom == 'object' && typeof vdom.type == 'string') {
        const dom = document.createElement(vdom.type);
        for (const child of [].concat(...vdom.children)) // flatten
            dom.appendChild(render(child));
        for (const prop in vdom.props)
            setAttribute(dom, prop, vdom.props[prop]);
        return mount(dom);
    } else {
        throw new Error(`Invalid VDOM: ${vdom}.`);
    }
};

const setAttribute = (dom, key, value) => {
    if (typeof value == 'function' && key.startsWith('on')) {
        const eventType = key.slice(2).toLowerCase();
        dom.__gooactHandlers = dom.__gooactHandlers || {};
        dom.removeEventListener(eventType, dom.__gooactHandlers[eventType]);
        dom.__gooactHandlers[eventType] = value;
        dom.addEventListener(eventType, dom.__gooactHandlers[eventType]);
    } else if (key == 'checked' || key == 'value' || key == 'id') {
        dom[key] = value;
    } else if (key == 'key') {
        dom.__gooactKey = value;
    } else if (typeof value != 'object' && typeof value != 'function') {
        dom.setAttribute(key, value);
    }
};

Patching

想象一个你有一个很深的结构,而且你还需要频繁的更新你的虚拟dom,如果你改变了一些,那么全部都要渲染,这无疑会消耗很多时间。

但是如果我们有一个算法能够比较出新的虚拟dom和已有dom的差异,然后只更新那些改变的地方,这个地方就是经常说的React团队做了一些经过实践后的约定,将本来o(n)^3的时间复杂度降低到了o(n),主要就是下面两种主要的约定

  • 两个元素如果有不同的类型那么就会产生两种不同的树
  • 当我们给了一个key属性后,他就会根据它去判断
const patch = (dom, vdom, parent=dom.parentNode) => {
    const replace = parent ? el => (parent.replaceChild(el, dom) && el) : (el => el);
    if (typeof vdom == 'object' && typeof vdom.type == 'function') {
        return Component.patch(dom, vdom, parent);
    } else if (typeof vdom != 'object' && dom instanceof Text) {
        return dom.textContent != vdom ? replace(render(vdom)) : dom;
    } else if (typeof vdom == 'object' && dom instanceof Text) {
        return replace(render(vdom));
    } else if (typeof vdom == 'object' && dom.nodeName != vdom.type.toUpperCase()) {
        return replace(render(vdom));
    } else if (typeof vdom == 'object' && dom.nodeName == vdom.type.toUpperCase()) {
        const pool = {};
        const active = document.activeElement;
        for (const index in Array.from(dom.childNodes)) {
            const child = dom.childNodes[index];
            const key = child.__gooactKey || index;
            pool[key] = child;
        }
        const vchildren = [].concat(...vdom.children); // flatten
        for (const index in vchildren) {
            const child = vchildren[index];
            const key = child.props && child.props.key || index;
            dom.appendChild(pool[key] ? patch(pool[key], child) : render(child));
            delete pool[key];
        }
        for (const key in pool) {
            if (pool[key].__gooactInstance)
                pool[key].__gooactInstance.componentWillUnmount();
            pool[key].remove();
        }
        for (const attr of dom.attributes) dom.removeAttribute(attr.name);
        for (const prop in vdom.props) setAttribute(dom, prop, vdom.props[prop]);
        active.focus();
        return dom;
    }
};

Component

   组件是最像js中函数的概念了,我们通过它能够展示出什么应该展示在屏幕上,它可以被定义成一个无状态的函数,或者是一个有生命周期的组件。
class Component {
    constructor(props) {
        this.props = props || {};
        this.state = null;
    }

    static render(vdom, parent=null) {
        const props = Object.assign({}, vdom.props, {children: vdom.children});
        if (Component.isPrototypeOf(vdom.type)) {
            const instance = new (vdom.type)(props);
            instance.componentWillMount();
            instance.base = render(instance.render(), parent);
            instance.base.__gooactInstance = instance;
            instance.base.__gooactKey = vdom.props.key;
            instance.componentDidMount();
            return instance.base;
        } else {
            return render(vdom.type(props), parent);
        }
    }

    static patch(dom, vdom, parent=dom.parentNode) {
        const props = Object.assign({}, vdom.props, {children: vdom.children});
        if (dom.__gooactInstance && dom.__gooactInstance.constructor == vdom.type) {
            dom.__gooactInstance.componentWillReceiveProps(props);
            dom.__gooactInstance.props = props;
            return patch(dom, dom.__gooactInstance.render());
        } else if (Component.isPrototypeOf(vdom.type)) {
            const ndom = Component.render(vdom);
            return parent ? (parent.replaceChild(ndom, dom) && ndom) : (ndom);
        } else if (!Component.isPrototypeOf(vdom.type)) {
            return patch(dom, vdom.type(props));
        }
    }

    setState(nextState) {
        if (this.base && this.shouldComponentUpdate(this.props, nextState)) {
            const prevState = this.state;
            this.componentWillUpdate(this.props, nextState);
            this.state = nextState;
            patch(this.base, this.render());
            this.componentDidUpdate(this.props, prevState);
        } else {
            this.state = nextState;
        }
    }

    shouldComponentUpdate(nextProps, nextState) {
        return nextProps != this.props || nextState != this.state;
    }

    componentWillReceiveProps(nextProps) {
        return undefined;
    }

    componentWillUpdate(nextProps, nextState) {
        return undefined;
    }

    componentDidUpdate(prevProps, prevState) {
        return undefined;
    }

    componentWillMount() {
        return undefined;
    }

    componentDidMount() {
        return undefined;
    }

    componentWillUnmount() {
        return undefined;
    }
}

本次文章中新开发的gooact轮子就结束了,让我们看看他有什么功能

  • 它能够高效的更新复杂的dom结构
  • 支持函数式和状态式两种组件

那它距离一个完整的React应用还差什么呢?

  • 他还不支持fragments,portals这样的新版本的特性
  • 因为React Fiber太复杂了,目前还没有支持
  • 如果你写了重复的key,可能会有bug
  • 对于一些方法,还少了一些回调函数
    但是这篇文章是不是给你带来一个全新的视角看React框架,让你对这个框架做的事情有了一个全局的了解呢?
    反正笔者看了原文对React框架思路又更加清晰了,最后献上使用这个框架的用例demo

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions