90行代码构建属于你的React

原文连接: dev.to/ameerthehac…javascript

本篇翻译已征得原做者赞成:java

更多文章可戳: github.com/YvetteLau/B…react

译者注:webpack

本文中的实现借助了 snabbdom ,所以若是你的关注点是虚拟DOM的实现或是将虚拟DOM渲染到浏览器的底层实现,本篇文章中并不会涉及到。有些人可能对此感动失望,可是,一口吃不成一个胖子,咱们须要一步一步来。git


正文:github

我没法理解我不能创造的东西 —— 费曼web

当我学习 React 的时候,我以为它所作的一切都是魔术,而后我就开始思考这种魔术到底是什么。我感到很是惊讶,当我发现 React 所作的一切很是简单,甚至若是咱们不是下一家大型初创公司增长筹码,仅须要不多的JS代码就能够构建它。这也是促使我写这篇文章的动力,但愿你读完这篇文章也有相同的感受。shell

咱们将构建什么功能?

  • JSX
  • 函数组件
  • 类组件
  • 生命周期钩子函数

咱们不会构建什么?

虚拟DOM

再次为了简单起见,咱们不会在本文中实现咱们本身的虚拟DOM,咱们将使用 snabbdom ,有趣的是,Vue.js 虚拟DOM借鉴了它,你能够在这里读更多关于 snabbdom 的内容: github.com/snabbdom/sn…npm

React Hooks

有些人可能对此感动失望,可是,一口吃不成一个胖子,咱们须要一步一步来,所以让咱们首先构建基本的东西,而后再在此基础上加以补充。我计划后续文章中在咱们这次构建的内容之上,编写咱们本身的 React Hooks 以及虚拟DOM,react-native

可调试性

这是增长任何库或框架的复杂度的关键部分之一,因为咱们只是出于娱乐目的而作,所以咱们能够放心地忽略 React 提供的可调试性功能,例如 dev tools 和分析器。

性能和兼容性

咱们不会过于关注咱们的库的性能,咱们只想构建能正常运行的库。让咱们也不要费力地确保它能够在市场上的全部浏览器上使用,只有可以在某些现代浏览器上可使用,那就已经很好了。

让咱们开始动手

在开始以前,咱们须要一个支持ES6,自动热更新的脚手架。我已经建立了一个很是基础的 webpack 脚手架,你能够进行克隆和设置: github.com/ameerthehac…

JSX

JSX 是一个开放标准,不只限于 React,咱们能够在没有 React 的状况下使用它,它比你想象得还有容易。想要了解如何让咱们的库支持 JSX ,咱们首先须要看看在咱们使用 JSX 时背后究竟发生了什么。

const App = (
    <div> <h1 className="primary">QndReact is Quick and dirty react</h1> <p>It is about building your own React in 90 lines of JavsScript</p> </div>
);

// 上面的 jsx 被转换成下面这样:
/** * React.createElement(type, attributes, children) */
var App = React.createElement(
    "div",
    null,
    React.createElement(
        "h1",
        {
            className: "primary"
        },
        "QndReact is Quick and dirty react"
    ),
    React.createElement(
        "p",
        null,
        "It is about building your own React in 90 lines of JavsScript"
    )
);
复制代码

正如你看到的,每一个 JSX 元素都经过 @babel/plugin-transform-react-jsx 插件转换为了 React.createElement(...) 函数调用的形式,你能够在这里使用 JSX 进行更多的转换

为了使上述转换运行正常,在编写 JSX 时,你须要引入 React,你就是为何当你不引入 React 时,编写 JSX 会出现错误的缘由。 @babel/plugin-transform-react-jsx 插件已经添加在了咱们的项目依赖中,下面咱们先安装一下依赖

npm install
复制代码

把项目的配置增长到 .babelrc 文件中:

{
    "plugins": [
        [
            "@babel/plugin-transform-react-jsx",
            {
                "pragma": "QndReact.createElement", // default pragma is React.createElement
                "throwIfNamespace": false // defaults to true
            }
        ]
    ]
}
复制代码

此后,只要 Babel 看到 JSX ,它就会调用 QntReact.createElement(...),可是咱们还未定义此函数,如今咱们将其写到 src/qnd-react.js 中。

const createElement = (type, props = {}, ...children) => {
    console.log(type, props, children);
};

// 像 React.createElement 同样导出
const QndReact = {
    createElement
};

export default QndReact;
复制代码

咱们在控制台打印出了传递给咱们的 typepropschildren。为了测试咱们的转换是否正常,咱们能够在 src/index.js 中编写一些 JSX

// QndReact 须要被引入
import QndReact from "./qnd-react";

const App = (
    <div> <h1 className="primary"> QndReact is Quick and dirty react </h1> <p>It is about building your own React in 90 lines of JavsScript</p> </div>
);
复制代码

启动项目: npm start,在浏览器输入localhost:3000,如今你的控制台看起来应该与下图相似:

根据以上信息,咱们可使用 snabbdom 建立咱们内部的 虚拟DOM节点 ,而后咱们才能将其用于咱们的协调(reconciliation) 过程,可使用以下的命令安装 snabbdom:

npm install snabbdom
复制代码

QndReact.createElement(...) 被调用时吗,建立和返回 虚拟DOM节点

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    return h(type, { props }, children);
};

const QndReact = {
    createElement
};

export default QndReact;
复制代码

很好,如今咱们能够解析 JSX 并建立本身的虚拟DOM节点,可是仍然没法在浏览器中呈现出来。为此,咱们在 src/qnd-react-dom.js 添加一个 render 方法。

//src/qnd-react-dom.js

//React.render(<App />, document.getElementById('root'));
const render = (el, rootElement) => {
    //将el渲染到rootElement的逻辑
}

const QndReactDom = {
    render
}
复制代码

与其咱们本身去处理将元素放到 DOM 上的繁重工做,不如让 snabbdom 去处理。为此咱们能够引入模块去初始化 snabbdomsnabbdom 中的模块能够看作是插件,能够支持 snabbdom 作更多的事。

//src/qnd-react-dom.js
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';
const reconcile = snabbdom.init([propsModule]);

const render = (el, rootDomElement) => {
    //将el渲染到rootElement
    reconcile(rootDomElement, el);
}

const QndReactDom = {
    render
}
export default QndReactDom;
复制代码

咱们使用这个新的 render 函数去 src/index 中去作一些魔法。

//src/index.js
import QndReact from "./qnd-react";
import QndReactDom from './qnd-react-dom';
const App = (
    <div> <h1 className="primary"> QndReact is Quick and dirty react </h1> <p>It is about building your own React in 90 lines of JavsScript</p> </div>
);

QndReactDom.render(App, document.getElementById('root'));
复制代码

瞧,咱们的JSX已经能够渲染到屏幕上了。

等下,这个有一个小问题,当咱们两次调用 render 时,咱们会在控制台看到一些奇怪的错误(译者注: 能够在 index.js 中屡次调用 render,查看控制台错误),背后的缘由是咱们只有在第一次渲染时,能够在真实的DOM节点上调用 reconcile 方法,而后,咱们应该在以前返回的虚拟DOM节点上调用。

//src/qnd-react-dom.js
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';

const reconcile = snabbdom.init([propsModule]);

let rootVNode;
//QndReactDom.render(App, document.getElementById('root'))
const render = (el, rootDomElement) => {
    if(rootVNode == null) {
        //第一次调用 render 时
        rootVNode = rootDomElement;
    }
    rootVNode = reconcile(rootVNode, el);
}

const QndReactDom = {
    render
}
export default QndReactDom;
复制代码

很开心,咱们的应用程序中有一个能正常工做的 JSX 渲染,如今让咱们开始渲染一个函数组件,而不只仅是一些普通的 HTML。

让咱们向 src/index.js 添加一个 Greeting 函数组件,以下所示:

//src/index.js
import QndReact from "./qnd-react";
import QndReactDom from './qnd-react-dom';

const Greeting = ({ name }) => <p>Welcome {name}!</p>;

const App = (
    <div> <h1 className="primary"> QndReact is Quick and dirty react </h1> <p>It is about building your own React in 90 lines of JavsScript</p> <Greeting name={"Ameer Jhan"} /> </div> ); QndReactDom.render(App, document.getElementById('root')); 复制代码

此时,在控制台会出现如下错误:

咱们能够在 QndReact.createElement(...) 方法中打印出数据看一下缘由。

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    console.log(type, props, children);
    return h(type, { props }, children);
};
...

复制代码

若是能够看到,函数组件传递过来的 type 是一个JS函数。若是咱们调用这个函数,就能得到组件但愿渲染的 HTML 结果。

咱们根据 type 参数的类型,若是是函数类型,咱们就调用这个函数,并将 props 做为参数传给它,若是不是函数类型,咱们就看成普通的 HTML 元素处理。

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    //若是是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};

const QndReact = {
    createElement
};

export default QndReact;
复制代码

欢呼!咱们的函数组件已经能够正常工做了。

咱们已经完成了不少,让咱们深吸一口气,喝杯咖啡,由于咱们已经差很少实现了 React,不过咱们还须要攻克类组件。

咱们首先在 src/qnd-react.js 中建立 Component 基类:

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    //若是是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};


class Component {
    constructor() { }

    componentDidMount() { }

    setState(partialState) { }

    render() { }
}


const QndReact = {
    createElement,
    Component
};

export default QndReact;
复制代码

如今咱们在 src/counter.js 中编写咱们的第一个 Counter 类组件:

//src/counter.js
import QndReact from './qnd-react';

export default class Counter extends QndReact.Component {
    constructor(props) {
        super(props);

        this.state = {
            count: 0
        }
    }

    componentDidMount() {
        console.log('Component mounted');
    }

    render() {
        return <p>Count: {this.state.count}</p>
    }
}
复制代码

是的,我知道咱们还没有在计数器中实现任何逻辑,可是别担忧,一旦咱们的状态管理系统运行正常,咱们就会添加这些内容。如今,让咱们尝试在 src/index.js 中渲染它。

//src/index.js
import QndReact from "./qnd-react";
import QndReactDom from './qnd-react-dom';
import Counter from "./counter";

const Greeting = ({ name }) => <p>Welcome {name}!</p>;

const App = (
    <div> <h1 className="primary"> QndReact is Quick and dirty react </h1> <p>It is about building your own React in 90 lines of JavsScript</p> <Greeting name={"Ameer Jhan"} /> <Counter /> </div> ); QndReactDom.render(App, document.getElementById('root')); 复制代码

和料想中的同样,又又又报错了。

上面的错误看起来是否是很熟悉,当你尝试使用类组件而不集成自 React.Component 时,可能遇到过以上错误。要知道为何会这样,咱们能够在 React.createElement(...) 中添加一个 console.log,以下所示:

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    console.log(typeof (type), type);
    //若是是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};
复制代码

咱们来看看控制台打印了什么内容。

你能够看出 Countertype 类型也是函数,这是由于 Babel 会将 ES6 类转换为普通的 JS 函数,那么咱们该如何类组件的状况呢。其实,咱们能够在咱们的 Component 基类中添加一个静态属性,这样咱们利用该属性去检查 type 参数是不是一个类。React 中也是相同的处理逻辑,你能够阅读 Dan的博客

//src/qnt-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    console.log(typeof (type), type);
    //若是是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};


class Component {
    constructor() { }

    componentDidMount() { }

    setState(partialState) { }

    render() { }
}
//给 Component 组件添加静态属性来区分是函数仍是类 
Component.prototype.isQndReactClassComponent = true;

const QndReact = {
    createElement,
    Component
};

export default QndReact;
复制代码

如今,咱们在 QndReact.createElement(...) 中增长一些代码来处理类组件。

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    console.log(type.prototype);
    /** * 若是是类组件 * 1.建立一个实例 * 2.调用实例的 render 方法 */
    if (type.prototype && type.prototype.isQndReactClassComponent) {
        const componentInstance = new type(props);

        return componentInstance.render();
    }
    //若是是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};


class Component {
    constructor() { }

    componentDidMount() { }

    setState(partialState) { }

    render() { }
}
//给 Component 组件添加静态属性来区分是函数仍是类 
Component.prototype.isQndReactClassComponent = true;

const QndReact = {
    createElement,
    Component
};

export default QndReact;
复制代码

如今,咱们的类组件已经可以渲染到浏览器上了:

咱们向类组件中增长 state,在此以前,咱们须要知道,每次调用 this.setState({}) 时,如何更新 DOM 的责任是 react-dom 包,而不是 React 的责任。这是为了使 React 的核心部分,例如Component 类与平台分离,从而提高代码的可重用性。即在 ReactNative 中,你也可使用一样的 Component 类,react-native 负责如何更新UI。你可能会问本身:当调用 this.setState(...) 时,React 如何知道该怎么作,答案就是 react-dom 经过在 React 上设置了一个 __updater 属性与 React 进行通讯。Dan 对此也有出色的文章,你能够点击阅读。如今让咱们在 QndReactDom 中为 QndReact 添加 __updater 属性。

//src/qnd-react-dom.js
import QndReact from './qnd-react';
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';

...

//QndReactDom 告诉 QndReact 如何更新 DOM
QndReact.__updater = () => {
    //当调用 this.setState 的时候更新 DOM 逻辑
}
复制代码

不管什么时候咱们调用 this.setState({...}),咱们都须要比较组件的 oldVNode 和在组件上调用了 render 方法以后生成的 newVNode。为了进行比较,咱们在类组件上添加 __vNode 属性,以维护该组件当前的 VNode 实例。

//src/qnd-react.js
...
const createElement = (type, props = {}, ...children) => {
    /** * 若是是类组件 * 1.建立一个实例 * 2.调用实例的 render 方法 */
    if (type.prototype && type.prototype.isQndReactClassComponent) {
        const componentInstance = new type(props);
        componentInstance.__vNode = componentInstance.render();
        return componentInstance.__vNode;
    }
    //若是是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};
...
复制代码

如今咱们来在 Component 的基类中实现 setState 方法。

//src/qnd-react.js
...
class Component {
    constructor() { }

    componentDidMount() { }

    setState(partialState) { 
        this.state = {
            ...this.state,
            ...partialState
        }
        //调用 QndReactDom 提供的 __updater 方法
        QndReact.__updater(this);
    }

    render() { }
}
...
复制代码

处理 QndReactDom 中的 __updater 方法。

//src/qnd-react-dom.js
...
QndReact.__updater = (componentInstance) => {
    //当调用 this.setState 的时候更新 DOM 逻辑
    //获取在 __vNode 上存储的 oldVNode
    const oldVNode = componentInstance.__vNode;
    //获取 newVNode
    const newVNode = componentInstance.render();
    //更新 __vNode
    componentInstance.__vNode = reconcile(oldVNode, newVNode);
}
...
export default QndReactDom;
复制代码

OK,咱们在 Counter 组件中增长 state 来检验咱们的 setState 实现是否生效。

//src/counter.js
import QndReact from './qnd-react';

export default class Counter extends QndReact.Component {
    constructor(props) {
        super(props);

        this.state = {
            count: 0
        }

        // update the count every second
        setInterval(() => {
            this.setState({
                count: this.state.count + 1
            })
        }, 1000);
    }

    componentDidMount() {
        console.log('Component mounted');
    }

    render() {
        return <p>Count: {this.state.count}</p>
    }
}
复制代码

太棒啦,如今 Counter 组件运行状况与咱们预期彻底一致。

咱们继续添加 componentDidMount 的生命周期钩子函数。 Snabbdom 提供了一些钩子函数,经过他们,咱们能够知道真实DOM上面是否有添加,删除或是更新了虚拟DOM节点,你能够在此处了解更多信息。

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    /** * 若是是类组件 * 1.建立一个实例 * 2.调用实例的 render 方法 */
    if (type.prototype && type.prototype.isQndReactClassComponent) {
        const componentInstance = new type(props);
        componentInstance.__vNode = componentInstance.render();
        return componentInstance.__vNode;

        //增长钩子函数(当虚拟DOM被添加到真实DOM节点上时)
        componentInstance.__vNode.data.hook = {
            create: () => {
                componentInstance.componentDidMount()
            }
        }
    }
    //若是是函数组件,那么调用它,并返回执行结果
    if (typeof (type) == 'function') {
        return type(props);
    }
    return h(type, { props }, children);
};

...

export default QndReact;

复制代码

至此,咱们已经在类组件上支持了 componentDidMount 生命周期钩子函数。

结束以前,咱们再添加下事件绑定的支持。为此,咱们能够在 Counter 组件中增长一个按钮,点击的时候,计数器的数字增长。请注意,咱们遵循的是基于常规的JS事件命名约定,而非基于 React,即双击事件使用 onDblClick,而非 onDoubleClick

import QndReact from './qnd-react';

export default class Counter extends QndReact.Component {
    constructor(props) {
        super(props);

        this.state = {
            count: 0
        }
    }

    componentDidMount() {
        console.log('Component mounted');
    }

    render() {
        return (
            <div> <p>Count: {this.state.count}</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}>Increment</button> </div>
        )
    }
}
复制代码

上面的组件不会正常工做,由于咱们没有告诉咱们的 VDom 如何去处理它。首先,咱们给 Snabdom 增长事件监听模块。

//src/qnd-react-dom.js
import QndReact from './qnd-react';
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';
import eventlistenersModule from 'snabbdom/modules/eventlisteners';

const reconcile = snabbdom.init([propsModule, eventlistenersModule]);

...

复制代码

Snabdom 但愿将文本属性和事件属性做为两个单独的对象,咱们咱们须要这样作:

//src/qnd-react.js
import { h } from 'snabbdom';

const createElement = (type, props = {}, ...children) => {
    ...

    let dataProps = {};
    let eventProps = {};

    for (let propKey in props) {
        // event 属性老是以 `on` 开头
        if (propKey.startsWith('on')) {
            const event = propKey.substring(2).toLowerCase();
            eventProps[event] = props[propKey];
        } else {
            dataProps[propKey] = props[propKey];
        }
    }
    return h(type, { props: dataProps, on: eventProps }, children);
};

...

复制代码

如今当咱们点击 Counter 组件的按钮的时候,计数器加1。

太棒了,咱们终于完成了一个React的简陋的实现。可是,咱们还不能呈现列表,我想把它做为有趣的小任务交给您。我建议您尝试在 src/index.js 中呈现一个列表,而后调试 QndReact.createElement(...) 方法找出问题所在。

感谢您一直陪伴我,但愿您喜欢构建本身的 React ,并了解了 React 在此过程当中是如何工做的。若是您在任何地方卡住了,请随时参考我共享的代码: github.com/ameerthehac…


关注公众号,加入技术交流群

相关文章
相关标签/搜索