精讀《用160行js代碼實現一個React》

如今網上有不少react原理解析這樣的文章,可是每每這樣的文章我看完事後卻沒有什麼收穫,由於行文思路太快,大部分就是寫了幾句話簡單介紹下這段代碼是用來幹嗎的,而後就貼上源碼讓你本身看,有可能做者本人是真的看懂了,可是對於大部分閱讀這篇文章的人來講,確是雲裏霧裏。javascript

講解一個框架的源碼,最好的方式就是實現一個簡易版的,這樣在你實現的過程當中,讀者就能瞭解到你總體的思路,也就能站在更高的層面上對框架有一個總體的認知,而不是陷在一些具體的技術細節上。java

這篇文章就很是棒的實現了一個簡單的react框架,接下來屬於對原文的翻譯加上一些本身在使用過程當中的理解。node

首先先總體介紹經過這篇文章你能學到什麼--咱們將實現一個簡單的React,包括簡單的組件級api和虛擬dom,文章也將分爲如下四個部分react

  • Elements:在這一章咱們將學習JSX是如何被處理成虛擬DOM的
  • Rendering: 在這一小節咱們將想你展現如何將虛擬dom變成真實的DOM的
  • Patching: 在這一章咱們將向你展現爲何key如此重要,而且如何利用虛擬DOM對已存在的DOM進行批量更新
  • Components :最後一小節將告訴你React組件和他的生命週期

Element

元素攜帶者不少重要的信息,好比節點的type,props,children list,根據這些屬性,能渲染出咱們須要的元素,它的樹形結構以下git

{
    "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的語法github

/** @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編譯的時候用哪一個函數api

/** @jsx Gooact.createElement */
複製代碼

Rendering

這一節是將vdom渲染真實dombash

上一節咱們已經獲得了根據jsx語法得出的虛擬dom樹形結構,那麼就該將這個虛擬dom結構渲染成真實dombabel

那麼咱們在拿到一個樹形結構的時候,如何判斷這個節點應該渲染成真實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
相關文章
相關標籤/搜索