react源碼分析——自己實現render方法

實現createElement函數

上一章中有講到,js文件可以識別jsx語法,是利用插件調用 react內部的createElement方法,傳遞相應的參數,生成虛擬的dom對象。在自己搭建react-source項目中,我們就先建一個文件夾react,然後創建一個index.js文件

index.js文件暴露一個React對象,這個對象上面一定有一個方法,createElement方法。我們就先這麼寫:

const React = {
    createElement
}
// 生成虛擬的dom對象
function createElement(tag, attrs, ...children) {
    return {
        tag,
        attrs,
        children
    }
}
export default React;

再建一個react-dom文件夾。

src下面的index.js引入新建好的文件:

import React from './react'
import ReactDOM from './react-dom'

const ele = (
    <div className='active'>
        hello <h1>react</h1>
    </div>
)
console.log(ele);

ReactDOM.render(ele, document.getElementById('app'));

然後npm run dev,打開頁面,看到控制檯打印出來這樣的一個對象,這就我們所說的虛擬dom對象,createElement方法是在es6轉es5的過程中,插件去調用的,並且解析相應的參數傳遞給這個方法。

可以看到,createElement這個函數很簡單,只需要把傳遞進來的參數返回就好,這樣jsx轉換成了虛擬的DOM對象。

但是,看下頁面,並沒有任何展示,這是因爲react-dom中的render函數沒有寫任何的功能,也就是沒有把虛擬的DOM轉化爲html。

實現render函數

調用render的時候傳遞的第一個參數是ele,也就是一個虛擬的dom對象,這個值有幾種情況:

1,參數沒有傳遞

創建一個空的文本節點document.createTextNode('');

2,傳遞的是字符串,是一個文本節點

document.createTextNode(v),可以直接創建文本節點

3,傳遞的是虛擬的dom對象

獲取虛擬對象中的屬性

    const { tag = '', attrs = '', children = [] } = v;

分析一下tag情況

a:tag是dom標籤

直接創建一個dom元素,document.createElement(tag),標籤上可能有屬性,標籤內部還會有子節點,實現一個setArribute函數處理dom上的屬性。setArribute調用的時候,傳遞創建好的dom標籤,屬性和對應的屬性值。

屬性分4種情況:

  • class
  • style
  • 事件
  • 自定義屬性。

class

  // class
    if(key === 'className') {
        key = 'class';
    }

事件 

這種情況需要把onClick大寫的事件轉化爲原生的小寫,添加到dom上。

 // 事件
    if(/on\w+/.test(key)) {
        key = key.toLowerCase()
        dom[key] = value || ''
    }

樣式 style

  • style的值是字符串
  • style的值是對象

字符串;

<body>
    <div id='app' style="color: red"></div>
</body>
<script>
    var app = document.getElementById('app');
    console.log(app.style.cssText);
</script>

所以可以直接這麼處理:

if(!value || typeof value === 'string') {
            dom.style.cssText = value || ''
        }

如果是對象,需要枚舉屬性,判斷屬性值,如果是number類型的,需要拼接'px'。

自定義屬性

原來dom上存在相同的自定義屬性,可以直接替換

原來dom上沒有,並且傳遞了value值,則需要設置屬性dom.setAttribute(key,value)

咩有傳遞value,是需要移除這個自定義屬性, dom.removeAttribute(key)

完成的代碼如下:

function setArribute(dom, key, value) {
    // class
    if(key === 'className') {
        key = 'class';
    }
    // 事件
    if(/on\w+/.test(key)) {
        key = key.toLowerCase()
        dom[key] = value || ''
    } else if(key === 'style') { // 樣式
        if(!value || typeof value === 'string') {
            dom.style.cssText = value || ''
        } else if(value && typeof value === 'object') {
            for(let k in value) {
                if(typeof value[k] === 'number') {
                    dom.style[k] = value[k] + 'px'
                } else {
                    dom.style[k] = value[k] 
                }
            }
        }
    } else {
        if(key in dom) {
            dom[key] = value;
        }
        if(value) {
            dom.setAttribute(key,value)
        } else {
            dom.removeAttribute(key)
        }
    }

}

 

如果說傳遞的是一個函數組件,看下tag會是什麼?

import React from './react'
import ReactDOM from './react-dom'


function Home() {
    return (
        <div className='home'>
            hello <h1>react</h1>
        </div> 
    )
}
console.log(<Home title='home'/>)


ReactDOM.render(<Home title='home'/>, document.getElementById('app'));

可以看到tag是一個函數。

如果說是一個class 類呢?

import React from './react'
import ReactDOM from './react-dom'

class Home extends React.Component {
    render() {
        return (
            <div className='active'>
                'hello'
                <h1 >react</h1>
            </div>
        )
    }
}
console.log(<Home title='home' />)
ReactDOM.render(<Home title='home' />, document.getElementById('app'));

注意:也可以在react腳手架創建的項目種看下打印結果。 

tag也是一個函數,所以還有一種情況是tag是函數。

b, tag是函數

函數組件也轉化成了虛擬的DOM,只不過這個虛擬對象的tag是一個函數。

  • 創建一個組件
  • 設置組件的屬性

把tag和attrs傳遞給createComponent函數。這個函數接收到參數以後,需要先判斷一下這個tag是class類生成的,還是函數生成的。

區分函數組件和class組件:函數組件沒有render函數,而class類組件原型上有render函數。

類組件:直接new comp(props)創建一個實例,屬性傳遞過去。

函數組件:定義一個空的class類,創建一個實例,修改實例的constructor,給實例增加一個render方法,內部調用函數組件,傳遞props。

function createComponent(comp, props) {
    let inst;
    // 如果是類定義的組件
    if(comp.prototype && comp.prototype.render) {
        return inst = new comp(props);
    }
    // 如果是函數,這裏我們需要轉化爲類,方便後面統一管理
    inst = new Component(props);
    inst.constructor = comp;
    inst.render = function() {
        return this.constructor(props);
    }
    return inst;
}

src/react/component.js

class Component {
    constructor(props = {}){
        this.props = props;
        this.state={}
    }
}
export default Component;

組件創建好以後,還需要渲染組件,把組件變成真是的dom。先給組件設置一個props屬性,再來渲染組件。

怎麼把組件html節點變成虛擬的dom的對象呢?組件內部都有render函數,返回一串jsx代碼,可以調用這個函數拿到jsx代碼快,也就生成虛擬的dom對象了。

給組件實例增加一個屬性props。

function setComeponentProps(comp, props) {
    // 設置組件的props
    comp.props = props;
    renderComponent(comp)
}

然後渲染組件

function renderComponent(comp) {
    // v虛擬的dom對象
    const v = comp.render();
    // 生成真實的dom
    comp.base = _render(v);
}

 4,children有值

如果children是有值的,說明還有字節點需要渲染,這時只需要循環遞歸調用_ender就可以了。

初步代碼如下:

function _render(v) {
    if(v === undefined || v === null || typeof v === 'boolean') return document.createTextNode('');

    // 如果是數值
    if(typeof v === 'number') v = String(v)

    // 如果是字符串
    if(typeof v === 'string') {
        return document.createTextNode(v);
    }
    // 如果tag是函數,則渲染組件
    const { tag = '', attrs = '', children = [] } = v;
    if(typeof tag === 'function') {
        // 創建組件
        const comp = createComponent(tag, attrs)
        // 設置組件屬性
        setComeponentProps(comp, attrs)

        // 組件渲染的節點返回
        return comp.base;

    }
    // 是一個虛擬dom對象

    // const { tag = '', attrs = '', children = [] } = v;

    const dom = document.createElement(tag);

    if(attrs) {
        Object.keys(attrs).forEach((key) => {
            const value = attrs[key];
            setArribute(dom, key, value);
        })
    }

    isArray(children) && children.forEach((child)=>render(child, dom))
    return dom



}

function isType(type) {
    return function(obj) {
      return {}.toString.call(obj) == "[object " + type + "]"
    }
  }
  
var isObject = isType("Object")
var isString = isType("String")
var isArray = Array.isArray || isType("Array")
var isFunction = isType("Function")
var isUndefined = isType("Undefined")

 

看,我們的頁面又能正常展示了。此時的src/index.js

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章