Tạo một thư viện giống React 16.3 với gần 160 dòng code javascript

React là một thư viện tốt, trên thế giới đã có rất nhiều người sử dụng react để làm các web app rất là tốt, ở bravebits cũng đang phát triển một sản phẩm page builder được viết bằng react như vậy,  đủ cho ta thấy được sức mạnh của nó, số lượng star mới đây là cán mốc hơn 100k .Hiện nay, rất nhiều người dùng react nhưng có ít ai quan tâm tới quá trình nó được tạo ra như thế nào .

Bài viết lấy cảm hứng từ việc mình tò mò về react và cũng có đọc qua một bài viết trên medium, mình tìm hiểu và thử code một thư viện na ná react, có lifecycle, state, setState, render, ref, props, … , và đây  là repo của nó : repo

Việc viết ra một thư viện hay tìm hiểu cách thư viện làm việc có vẻ rất ít người để ý tới, mình trước cũng nghĩ thế, đến khi mình vọc vào nó thì mình thấy ta có thể học được rất nhiều điều từ nó, chúng ta có thể đụng vào các syntax khó hiểu và các thuật toán phức tạp, điển hình là trong react mình thấy nó sử dụng rất nhiều thuật toán backtracking , OK chúng ta sẽ đi thẳng vào vấn đề

Đã nhắc đến react thì chắc chắn có props, lifecycle, class component, render, state. Chúng ta sẽ lấy đó làm đề bài cho chính mình

Nói qua về react, react sử dụng vitural dom, những component bạn viết trong render đều là vitual dom, nó không phải là dom, trong project này , mình sẽ sử dụng jsx và config lại nó một chút ở trong babel

{
  "presets": ["stage-2"],
  "plugins": [
    "transform-class-properties",
    "transform-object-rest-spread",
    [
      "transform-react-jsx",
      {
        "pragma": "fakeReact.createElement"
      }
    ]
  ]
}

Thay vì sử dụng React.createElement thì mình sử dụng fakeReact, fakeReact trong đó là code thư viện của mình

Vậy chúng ta đặt câu hỏi: Vậy createElement là function gì ?

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

Đây chính là nó, cơ bản là mình sẽ check props và trả về cái object có dạng {type, props, children}, trong đó type là kiểu của vdom, nó có thể là tên một class, string, number và tên của một tag html(ul , li ,a ,… )

Bản chất cuối cùng, mọi thứ của mình cũng chỉ để tạo ra hệ thống vdom này

Example:

const vdom = {
  type : 'ul',
  props : '',
  chidlren : [
    {
      type : 'li',
      props : '',
      children :'bla bla'
    }
  ]
}

Đoạn này chính ra sẽ render ra <ul><li>bla bla</li></ul>

Vậy render và setAttribute như thế nào ?

Để viết giống react thì ta phải hiểu react, khi ta viết một component thì chúng ta đều phải extends từ react component, vậy từ tư tưởng đó thì mình code cũng phải đạt được như thế. Vậy làm như thế nào để chúng ta có thể render ra dom từ cục vdom xấu hoắc đó, ở đoạn này mình giải quyết bằng cách sử dụng backtracking , chúng ta sẽ check và đệ quy từng trường hợp, khi nào đó khi children là string thì dừng .Đây là hàm render, bạn có thể đọc comment để biết chi tiết hơn

const render = (vdom, parent=null) => {
    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') {
        // Với Trường này thì sẽ phải đệ quy đến khi nào hết cái vdom.type == 'function ' thì thôi, vì nó đang ở trạng thái class, 
        // chúng ta phải mò vào bên trong hàm render của component , lấy dom nó ra là check cái dom của nó tiếp
        console.log("check vdom.type function", vdom.type)
        // if vdom.type is class
        return Component.render(vdom, parent);
    } else if (typeof vdom == 'object' && typeof vdom.type == 'string') {
        // if vdom.type is string => return dom .. this case only return dom
        const dom = mount(document.createElement(vdom.type));
        console.log("vdom.type string" , vdom)
        //backtrack algo
        for (const child of [].concat(...vdom.children)) render(child, dom);
        for (const prop in vdom.props) setAttribute(dom, prop, vdom.props[prop]);
        console.log("fullll " , dom)
        return dom;
    } else {
        throw new Error(`Invalid VDOM: ${vdom}.`);
    }
};

Hiểu đơn giản function này, mình sẽ truyền dom vào, key là tên attribute và value là giá trị gán cho value đó. Ở project, có một điều khá thú vị là mình sẽ lưu hết các instance của class component, key, type của nó, để tí nữa mình có thể share cho các phần khác và các bạn chú ý phần render, nó share 3 thứ quan trọng  là class, instance của class, type và tí nữa từ dom mình lấy ra để dùng cho lifecycle 🙂

const setAttribute = (dom, key, value) => {
    //nếu nó là function và bắt đầu bằng chữ on thì ...
    if (typeof value == 'function' && key.startsWith('on')) {
        // loại chứ on ra là đưa và string lowercase
        const eventType = key.slice(2).toLowerCase();
        dom._FakeReactHandlers = dom._FakeReactHandlers || {};
        dom.removeEventListener(eventType, dom._FakeReactHandlers[eventType]);
        console.dir(dom)
        dom._FakeReactHandlers[eventType] = value;
        dom.addEventListener(eventType, dom._FakeReactHandlers[eventType]);
       // check để các TH đặc biệt của key
    } else if (key == 'checked' || key == 'value' || key == 'className') {
        dom[key] = value;
        
    } else if (key == 'style' && typeof value == 'objec t') {
        Object.assign(dom.style, value);
        
        
        //nếu nó là ref thì truyền dom ra ngoài
    } else if (key == 'ref' && typeof value == 'function') {
        value(dom);
    } else if (key == 'key') {
        // lưu key và object tự 
        dom._FakeReactKey = value;
    } else if (typeof value != 'object' && typeof value != 'function') {
        dom.setAttribute(key, value);
    }
};

Update Như thế nào ?

Tiếp theo đến vấn đề update, vấn đề update ở đây mình thấy khá là thú vị, vậy chúng ta phải tự hỏi, nó update khi nào. Nó update khi state thay đổi, props thay đổi, Vâỵ dựa vào những giả thiết đó để chúng ta quá trình như sau

Quá trình update gồm 2 phần

  • Xoá dom cũ
  • Render lại
  • So sánh cái hiện tại và cái cũ để update
  • Chạy các hàm lifecycle

Quá trình update thì mình lại tiếp tục sử dụng backtracking đẻ tìm dom thay đổi và update, Mọi lời giải thích cần thiết mình sẽ viết thẳng vào code cho nó trực quan và dễ hiểu 🙂

const patch = (dom, vdom, parent=dom.parentNode) => {
    console.log("tesst dom ", dom , "tesst vdom ", vdom , "parent" , parent)
    // hàm repalce này sẽ giúp mình  nối dom với dom cha 
    const replace = parent ? el => (parent.replaceChild(el, dom) && el) : (el => el);
    // case vdom.type is class , and vdom object
    // sử dụng thuật tóan backtracking
    if (typeof vdom == 'object' && typeof vdom.type == 'function') {
        return Component.patch(dom, vdom, parent);
    } 
    //vdom != object tức là 
    else if (typeof vdom != 'object' && dom instanceof Text) {
        return dom.textContent != vdom ? replace(render(vdom, parent)) : dom;
    } 
    
    else if (typeof vdom == 'object' && dom instanceof Text) {
        return replace(render(vdom, parent));
    } 
    else if (typeof vdom == 'object' && dom.nodeName != vdom.type.toUpperCase()) {
        return replace(render(vdom, parent));
    } 
    else if (typeof vdom == 'object' && dom.nodeName == vdom.type.toUpperCase()) {
        const pool = {};
        const active = document.activeElement;
        [].concat(...dom.childNodes).map((child, index) => {
            const key = child._FakeReactKey || `index_${index}`;
            pool[key] = child;
        });
        [].concat(...vdom.children).map((child, index) => {
            const key = child.props && child.props.key || `__index_${index}`;
            dom.appendChild(pool[key] ? patch(pool[key], child) : render(child, dom));
            delete pool[key];
        });
        for (const key in pool) {
            // lấy instance , xem lại hàm render để biết _FakeReactInstance lấy từ đâu , nó là object lưu instance của class
            const instance = pool[key]._FakeReactInstance;
            if (instance) instance.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;
    }
};

Chúng ta còn mỗi class component nữa, mọi thứ đều được extends từ class component, mỗi class được extends từ  class component, ở đây mình sẽ định nghĩa các lifecycle, setState, ..

class Component {
    constructor(props) {
        // khơi tạo props và state
        this.props = props || {};
        this.state = null;
    }
    //bản chất hàm này là nhận vdom và nó gọi thằng const render ở trên và backtracking ở trong đó, và render dựa vào vdom và
    //appendChild vào root 
    static render(vdom, parent=null) {
        //spead props , and vdom children => cũng là
          const props = Object.assign({}, vdom.props, {children: vdom.children});
          console.log("vdom . type " , vdom.type)
        if (Component.isPrototypeOf(vdom.type)) {
            const instance = new (vdom.type)(props); // instance is vdom 
            console.log("instance"  ,instance)
            // if(instance._FakeReactClass.getDerivedStateFromProps){
            //     instance._FakeReactClass.getDerivedStateFromProps(this.props, this.state)
            // }
           
           //save object base in instance, why ??
           // save instance in base, when setState , we will use object base get instance 
            instance.base = render(instance.render(), parent);
            // save instance and key
            instance.base._FakeReactClass  = vdom.type
            instance.base._FakeReactInstance = instance;
            instance.base._FakeReactKey = vdom.props.key;
            instance.componentDidMount();
            console.log("instance.base "  , instance.base)
            return instance.base;
        } else {
            // TH nó là function chẳng hạn mà ko phải là một class extends Component
            return render(vdom.type(props), parent);
        }
    }
   // thằng này cũng dựa tương tự như render 
    static patch(dom, vdom, parent=dom.parentNode) {
        const props = Object.assign({}, vdom.props, {children: vdom.children});
        if (dom._FakeReactInstance && dom._FakeReactInstance.constructor == vdom.type) {
            // Component.getDerivedStateFromProps(props, this.state);
            dom._FakeReactInstance.props = props;
            return patch(dom, dom._FakeReactInstance.render(), parent);
        } else if (Component.isPrototypeOf(vdom.type)) {
            const ndom = Component.render(vdom, parent);
            return parent ? (parent.replaceChild(ndom, dom) && ndom) : (ndom);
        } else if (!Component.isPrototypeOf(vdom.type)) {
            return patch(dom, vdom.type(props), parent);
        }
    }
    setState(next) {
            // check state is object and is  para object ??
        const compat = (a) => typeof this.state == 'object' && typeof a == 'object';
         // i want getDerivedStateFromProps check update
        if (this.base ) {
            if(this.base._FakeReactClass.getDerivedStateFromProps && this.base._FakeReactClass.getDerivedStateFromProps(this.state, this.props) ){
                const prevState = this.state;
                this.state = compat(next) ? Object.assign({}, this.state, next) : next; //new state
                this.shouldComponentUpdate(this.props ,this.state )
                patch(this.base, this.render()); // process render
                this.getSnapshotBeforeUpdate(this.props ,prevState )
                this.componentDidUpdate(this.props, prevState);
            }
            else if(this.base._FakeReactClass.getDerivedStateFromProps && !this.base._FakeReactClass.getDerivedStateFromProps(this.state, this.props)){

            }
            else if(Component.getDerivedStateFromProps(this.props, this.state)){
                const prevState = this.state;
                this.state = compat(next) ? Object.assign({}, this.state, next) : next; //new state
                this.shouldComponentUpdate(this.props ,this.state )
                patch(this.base, this.render()); // process render
                this.getSnapshotBeforeUpdate(this.props ,prevState )
                this.componentDidUpdate(this.props, prevState);
            }
            
        } else {
            this.state = compat(next) ? Object.assign({}, this.state, next) : next;
        }
    }
    // định nghĩa các hàm 
    getSnapshotBeforeUpdate(prevProps, prevState) {
        return nextProps != this.props || nextState != this.state;
    }

    static getDerivedStateFromProps(nextProps , preState) {
        return true;
    }

    shouldComponentUpdate(nextState, nextProps){
        return undefined;
    }
    componentDidUpdate(prevProps, prevState) {
        return undefined;
    }


    componentDidMount() {
        return undefined;
    }

    componentWillUnmount() {
        return undefined;
    }
};

NOTE:

  • Khi các bạn đọc hiểu là hàm static render không phải là hàm render trong class component mình viết đâu nha
  • Trong dom đều được share các instance, class, type, vì vậy chỉ cần cần có dom thì chúng ta sẽ có tất cả instance, class, type

Kết thúc:

Việc tìm tòi thư viện rất thú vị, thay vì chúng ta chỉ biết sử dụng chúng thì hãy tập viết lại chúng, nó sẽ giúp tăng khả năng sử dụng ngôn ngữ đó và khả năng code và tư duy giải thuật. Bài viết mình xin kết thúc tại đây, chỉ là ví dụ nhỏ thôi mà cũng khá nhiều vấn đề để viết rồi :). Cảm ơn các bạn đã đọc bài viết của mình

Comments

Let’s make a great impact together

Be a part of BraveBits to unlock your full potential and be proud of the impact you make.