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

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

现在网上有很多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

原文发布时间为:2018年6月8日

原文作者:2014_

本文来源:掘金如需转载请联系原作者

相关文章
|
6月前
|
编解码 前端开发 JavaScript
js react antd 实现页面低分变率和高分变率下字体大小自适用,主要是配置antd
在React中结合Ant Design与媒体查询,通过less变量和响应式断点动态调整`@font-size-base`,实现多分辨率下字体自适应,提升跨设备体验。
304 2
|
11月前
|
前端开发 JavaScript NoSQL
使用 Node.js、Express 和 React 构建强大的 API
本文详细介绍如何使用 Node.js、Express 和 React 构建强大且动态的 API。从开发环境搭建到集成 React 前端,再到利用 APIPost 高效测试 API,适合各水平开发者。内容涵盖 Node.js 运行时、Express 框架与 React 库的基础知识及协同工作方式,还涉及数据库连接和前后端数据交互。通过实际代码示例,助你快速上手并优化应用性能。
|
前端开发 JavaScript 测试技术
React 中集成 Chart.js 图表库
本文介绍了如何在 React 项目中集成 Chart.js 创建动态图表,涵盖基础概念、安装步骤、代码示例及常见问题解决方法,帮助开发者轻松实现数据可视化。
452 11
|
监控 前端开发 JavaScript
React 静态网站生成工具 Next.js 入门指南
【10月更文挑战第20天】Next.js 是一个基于 React 的服务器端渲染框架,由 Vercel 开发。本文从基础概念出发,逐步探讨 Next.js 的常见问题、易错点及解决方法,并通过具体代码示例进行说明,帮助开发者快速构建高性能的 Web 应用。
849 10
|
资源调度 前端开发 数据可视化
构建高效的数据可视化仪表板:D3.js与React的融合之道
【10月更文挑战第25天】在数据驱动的时代,将复杂的数据集转换为直观、互动式的可视化表示已成为一项至关重要的技能。本文深入探讨了如何结合D3.js的强大可视化功能和React框架的响应式特性来构建高效、动态的数据可视化仪表板。文章首先介绍了D3.js和React的基础知识,然后通过一个实际的项目案例,详细阐述了如何将两者结合使用,并提供了实用的代码示例。无论你是数据科学家、前端开发者还是可视化爱好者,这篇文章都将为你提供宝贵的洞见和实用技能。
464 5
|
开发框架 前端开发 JavaScript
React、Vue.js 和 Angular主流前端框架和选择指南
在当今的前端开发领域,选择合适的框架对于项目的成功至关重要。本文将介绍几个主流的前端框架——React、Vue.js 和 Angular,探讨它们各自的特点、开发场景、优缺点,并提供选择框架的建议。
477 6
|
前端开发 JavaScript 开发者
React 和 Vue.js 框架的区别是什么?
React 和 Vue.js 框架的区别是什么?
|
前端开发 JavaScript API
React、Vue.js 和 Angular前端三大框架对比与选择
前端框架是用于构建用户界面的工具和库,它提供组件化结构、数据绑定、路由管理和状态管理等功能,帮助开发者高效地创建和维护 web 应用的前端部分。常见的前端框架如 React、Vue.js 和 Angular,能够提高开发效率并促进团队协作。
1611 4
|
JavaScript 前端开发 开发者
JavaScript框架React vs. Vue:一场性能与易用性的较量
JavaScript框架React vs. Vue:一场性能与易用性的较量
403 0
|
前端开发 JavaScript 开发者
Express.js与前端框架的集成:React、Vue和Angular的示例与技巧
本文介绍了如何将简洁灵活的Node.js后端框架Express.js与三大流行前端框架——React、Vue及Angular进行集成,以提升开发效率与代码可维护性。文中提供了详细的示例代码和实用技巧,展示了如何利用Express.js处理路由和静态文件服务,同时在React、Vue和Angular中构建用户界面,帮助开发者快速掌握前后端分离的开发方法,实现高效、灵活的Web应用构建。
570 3

热门文章

最新文章