前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >精读《用160行js代码实现一个React》

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

作者头像
2014v
发布于 2019-11-20 06:27:24
发布于 2019-11-20 06:27:24
69700
代码可运行
举报
文章被收录于专栏:2014前端笔记2014前端笔记
运行总次数:0
代码可运行

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

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

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

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

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

Element

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
    "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的语法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/** @jsx createElement */
const list = <ul className="some-list">
    <li className="some-list__item">One</li>
    <li className="some-list__item">Two</li>
</ul>;

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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编译的时候用哪个函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/** @jsx Gooact.createElement */

Rendering

这一节是将vdom渲染真实dom

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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属性后,他就会根据它去判断
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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中函数的概念了,我们通过它能够展示出什么应该展示在屏幕上,它可以被定义成一个无状态的函数,或者是一个有生命周期的组件。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
手写简易前端框架:patch 更新(1.0 完结篇)
前面两篇文章,我们实现了 vdom 的渲染和 jsx 的编译,实现了 function 和 class 组件,这篇来实现 patch 更新。
神说要有光zxg
2022/03/03
3670
手写简易前端框架:patch 更新(1.0 完结篇)
​我用300行代码实现了React
我们先使用最新版create-react-app,在example/目录下创建一个demo项目:
孟健
2022/12/19
8620
​我用300行代码实现了React
最近几周react面试遇到的题总结
通俗点理解就是,vuex 弱化 dispatch,通过commit进行 store状态的一次更变;取消了action概念,不必传入特定的 action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;
beifeng1996
2022/09/25
8740
手写简易前端框架:vdom 渲染和 jsx 编译
作为前端工程师,前端框架几乎每天都要用到,需要好好掌握,而对某项技术的掌握程度可以根据是否能实现一个来判断。手写一个前端框架对更好的掌握它是很有帮助的事情。
神说要有光zxg
2022/03/03
4440
手写简易前端框架:vdom 渲染和 jsx 编译
手写简易前端框架:function 和 class 组件
上篇文章我们实现了 vdom 的渲染,这是前端框架的基础。但手写 vdom 太麻烦,我们又支持了 jsx,用它来写页面更简洁。
神说要有光zxg
2022/03/03
3490
手写简易前端框架:function 和 class 组件
一定要熟记这些常被问到的React面试题
要了解 JSX,首先先了解什么三个主要问题,什么事 VDOM,差异更新和 JSX 建模:
wscats
2020/06/06
1.4K0
带你实现react源码的核心功能
React 的代码还是非常复杂的,虽然这里是一个简化版本。但是还是需要有不错的面向对象思维的。React 的核心主要有一下几点。
goClient1992
2022/10/03
1.3K0
前端必会react面试题及答案
props 更新流程: 相对于 state 更新,props 更新后唯一的区别是增加了对 componentWillReceiveProps 的调用。关于 componentWillReceiveProps,需要知道这些事情:
beifeng1996
2022/12/12
8080
手写简易版 React 来彻底搞懂 fiber 架构
React 16 之前和之后最大的区别就是 16 引入了 fiber,又基于 fiber 实现了 hooks。整天都提 fiber,那 fiber 到底是啥?它和 vdom 是什么关系?
神说要有光zxg
2022/03/03
8440
手写简易版 React 来彻底搞懂 fiber 架构
前端react面试题总结
如果您尝试直接改变组件的状态,React 将无法得知它需要重新渲染组件。通过使用setState()方法,React 可以更新组件的UI。
beifeng1996
2022/10/29
2.7K0
百度前端高频react面试题总结
(2)如果已经创建了 Create React App 项目,需要将 typescript 引入到已有项目中
beifeng1996
2022/10/25
1.8K0
从零自己编写一个React框架 【中高级前端杀手锏级别技能】
为了降低本文难度,构建工具选择了parcel,欢迎加入我们的前端交流群~ gitHub仓库源码地址和二维码都会在最后放出来~
Peter谭金杰
2019/08/13
1K0
从零自己编写一个React框架    【中高级前端杀手锏级别技能】
react16常见api以及原理剖析
React 与 Vue 有很多相似之处,React 和 Vue 都是非常优秀的框架,它们之间的相似之处多过不同之处,并且它们大部分最棒的功能是相通的:如他们都是 JavaScript 的 UI 框架,专注于创造前端的富应用。不同于早期的 JavaScript 框架“功能齐全”,Reat 与 Vue 只有框架的骨架,其他的功能如路由、状态管理等是框架分离的组件。
前端迷
2019/09/25
1K0
react16常见api以及原理剖析
React中的JSX原理渐析
在react官方中讲到,关于jsx语法最终会被babel编译成为React.createElement()方法。
19组清风
2021/11/15
2.4K0
React中的JSX原理渐析
手写一个react,看透react运行机制
react的源码,的确是比vue的难度要深一些,本文也是针对初中级,本意让博友们了解整个react的执行过程。
goClient1992
2022/09/26
2.1K0
React生命周期简单分析
1.React16.3的发布带来了一些新的特性, 除了新的ContextAPI之外, 还对生命周期做了部分修改, 为了支持未来的异步渲染特性, 一下生命周期将被废弃
IMWeb前端团队
2019/12/03
1.3K0
React生命周期简单分析
前端必会react面试题合集2
(2)如果已经创建了 Create React App 项目,需要将 typescript 引入到已有项目中
beifeng1996
2023/01/04
2.4K0
京东前端二面高频react面试题
每个React组件强制要求必须有一个 render()。它返回一个 React 元素,是原生 DOM 组件的表示。如果需要渲染多个 HTML 元素,则必须将它们组合在一个封闭标记内,例如 <form>、<group>、<div> 等。此函数必须保持纯净,即必须每次调用时都返回相同的结果。
hellocoder2028
2022/09/14
1.6K0
一天梳理完React所有面试考察知识点
在shouldComponentUpdate()判断中,有一个有意思的问题,解释为什么 React setState() 要用不可变值
beifeng1996
2022/10/06
2.9K0
基于 React 实现一个 Transition 过渡动画组件
过渡动画使 UI 更富有表现力并且易于使用。如何使用 React 快速的实现一个 Transition 过渡动画组件?
用户6167509
2020/02/28
6.1K0
相关推荐
手写简易前端框架:patch 更新(1.0 完结篇)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验