按部就班DIY一個react(二)

承接上文,假如我給你一個virtual DOM對象,那麼你該如何實現將它渲染到真實的文檔中去呢?這個時候就涉及到原生DOM接口的一些增刪改查的知識點了:javascript

// 增:根據標籤名,建立一個元素節點(element node)
let divElement = document.createElement('div')

// 增:根據文本內容,建立一個文本節點(text node)
const textNode = document.createTextNode('我是文本節點')

// 查:經過一個id字符串來獲取文檔中的元素節點
const bodyElement = document.getElementsByTagName('body')[0]

// 改:設置元素節點的非事件類型的屬性(property)
divElement['id'] = 'test'
divElement['className'] = 'my-class'

// 改:給元素設置事件監聽器
divElement.addEventListener('click',() => { console.log('I been clicked!')})

// 改:改變文檔樹結構
divElement.appendChild(textNode)
bodyElement.appendChild(divElement)

// 刪:從文檔結構樹中刪除
bodyElement.removeChild(divElement)
複製代碼

上面有一個注意點,那就是咱們設置元素屬性的寫法是設置property而不是設置attibute。在DOM裏面,property和attribute是兩種概念。而設置property意味着只有有效的屬性纔會生效。html

在react中,「react element」是一個術語,指的就是一個virtual DOM對象。而且在react.js的源碼中,都是用element來指代的。爲了統一,咱們也使用elment這個名字來命名virtual DOM對象,以下:前端

const element = {
  type:'div',
  props:{
    id:'test',
    children:['我是文本節點']
  }
}

複製代碼

咱們暫時不考慮引入「component」這個概念,因此,type的值的類型是隻有字符串。由於有些文檔標籤是能夠沒有屬性的,因此props的值能夠是空對象(注意,不是null)。props的children屬性值是數組類型,數組中的每一項又都是一個react element。由於有些文檔標籤是能夠沒有子節點,因此,props的children屬性值也是能夠是空數組。這裏面咱們看到了一個嵌套的數據結構,可想而知,具體的現實裏面極可能會出現遞歸。java

你們有沒有發現,即便咱們不考慮引入「component」這個概念,咱們到目前爲止,前面所提的都是對應於element node的,咱們並無提到text node在virtual DOM的世界是如何表示的。咋一想,咱們可能會這樣設計:node

const element = {
  type:'我是文本節點',
  props:{}
}
複製代碼

從技術實現方面講,這是可行的。可是仔細思考後,這樣作顯然是混淆了當初定義type字段的語義的。爲了維持各字段(type,props)語義的統一化,咱們不妨這樣設計:react

const element = {
  type:'TEXT_ELEMENT',
  props:{
    nodeValue:'我是文本節點'
  }
}
複製代碼

這樣一來, text node和element node在virtual DOM的世界裏面都有了對應的表示形式了:DOMElement 和 textElement數組

// 元素節點表示爲:
const DOMElement = {
   type:'div',
   props:{
    id:'test',
    children:[
        {
            type:'TEXT_ELEMENT',
            props:{
                nodeValue:'我是文本節點'
       }
    ]
   }
}

// 文本節點表示爲:
const textElement = {
   type:'TEXT_ELEMENT',
   props:{
    nodeValue:'我是文本節點'
   }
}

複製代碼

對react element的數據結構補充完畢後,咱們能夠考慮具體的實現了。咱們就叫這個函數爲render(對應ReactDOM.render()方法)吧。根據咱們的需求,render函數的簽名大概是這樣的:bash

render : (element,domContainer) => void
複製代碼

細想之下,這個函數的實現邏輯的流程圖大概是這樣的:babel

那好,爲了簡便,咱們暫時不考慮edge case,並使用ES6的語法來實現這個邏輯:數據結構

function render(element,domContainer){
    const { type, props } = element
    
    // 建立對應的DOM節點
    const isTextElement = type === 'TEXT_ELEMENT'
    const domNode = isTextElement ? document.createTextNode('') : document.createElement(type)
    
    // 給DOM節點的屬性分類:事件屬性,普通屬性和children
    const keys = Object.keys(props)
    const isEventProp = prop => /^on[A-Z]/.test(prop)
    const eventProps = keys.filter(isEventProp) // 事件屬性
    const normalProps = keys.filter((key) => !isEventProp(key) && key !== 'children') // 普通屬性
    const children = props.children // children
    
    // 對事件屬性,添加對應的事件監聽器
    eventProps.forEach(name => {
        const eventType = name.toLowerCase().slice(2)
        const eventHandler = props[name]
        domNode.addEventListener(eventType,eventHandler)
    })
    
    // 對普通屬性,直接設置
     normalProps.forEach(name => {
        domNode[name] = props[name]
    })
    
    // 遍歷children,遞歸調用render函數
    if(children && children.length){
        children.forEach(child => render(child,domNode))
    }
    
    // 最終追加到容器節點中去
    domContainer.appendChild(domNode)
}
複製代碼

至此,咱們完成了從virtual DOM -> real DOM的映射的實現。如今,咱們能夠用如下的virtual DOM:

const element = {
    type:'div',
    props:{
    id:'test',
    onClick:() => { alert('I been clicked') },
    children:[
        {
            type:'TEXT_ELEMENT',
            props:{
                nodeValue:'我是文本節點'
            }
        }
    ]
    }
}
複製代碼

來映射這樣的文檔結構:

<div id="test" onClick={() => { alert('I been clicked')}>
    我是文本節點
</div>
複製代碼

你能夠把下面完整的代碼複製到codepen裏面驗證一下:

const element = {
            type: 'div',
            props: {
                id: 'test',
                onClick: () => { alert('I been clicked') },
                children: [
                    {
                        type: 'TEXT_ELEMENT',
                        props: {
                            nodeValue: '我是文本節點'
                        }
                    }
                ]
            }
        }

        function render(element, domContainer) {
            const { type, props } = element

            // 建立對應的DOM節點
            const isTextElement = type === 'TEXT_ELEMENT'
            const domNode = isTextElement ? document.createTextNode('') : document.createElement(type)

            // 給DOM節點的屬性分類:事件屬性,普通屬性和children
            const keys = Object.keys(props)
            const isEventProp = prop => /^on[A-Z]/.test(prop)
            const eventProps = keys.filter(isEventProp) // 事件屬性
            const normalProps = keys.filter((key) => !isEventProp(key) && key !== 'children') // 普通屬性
            const children = props.children // children

            // 對事件屬性,添加對應的事件監聽器
            eventProps.forEach(name => {
                const eventType = name.toLowerCase().slice(2)
                const eventHandler = props[name]
                domNode.addEventListener(eventType, eventHandler)
            })

            // 對普通屬性,直接設置
            normalProps.forEach(name => {
                domNode[name] = props[name]
            })

            // 遍歷children,遞歸調用render函數
            if (children && children.length) {
                children.forEach(child => render(child, domNode))
            }

            // 最終追加到容器節點中去
            domContainer.appendChild(domNode)
        }

        window.onload = () => {
            render(element, document.body)
        }
複製代碼

雖然咱們已經完成了基本映射的實現,可是你有沒有想過,假如咱們要用virtual DOM對象去描述一顆深度很深,廣度很廣的文檔樹的時候,那咱們寫javascript對象是否是要寫斷手啦?在這個Node.js賦能前端,語法糖流行的年代,咱們有沒有一些即優雅又省力的手段來完成這個工做呢?答案是:「有的,那就是JSX」。 說到這裏,那確定要提到無所不能的babel編譯器了。如今,我無心講babel基於Node.js+AST的編譯原理和它的基於插件的擴展機制。咱們只是假設咱們手上有一個叫transform-react-jsx的plugin。它可以把咱們寫的jsx:

const divElement = (
    <div id="test" onClick={() => { alert('I been clicked')}>
        我是文本節點1
        <a href="https://www.baidu.com">百度一下</a>
    </div>
)
複製代碼

編譯成對應的javascript函數調用:

const divElement = createElement(
    'div',
    {
        id:test,
        onClick:() => { alert('I been clicked') }
    },
    '我是文本節點',
    createElement(
        'a',
        {
            href:'https://www.baidu.com'
        },
        '百度一下'
    )
    )
複製代碼

而做爲配合,咱們須要手動實現這個createElement函數。從上面的假設咱們能夠看出,這個createElement函數的簽名大概是這樣的:

createElement:(type,props,children1,children2,...) => element
複製代碼

咱們已經約定好了element的數據結構了,如今咱們一塊兒來實現一下:

function createElement(type,props,...childrens){
    const newProps = Object.assign({},props)
    const hasChildren = childrens.length > 0 
    newProps.children = hasChildren ? [].concat(...childrens) : []
    return {
        type,
        props:newProps
    }
}
複製代碼

上面這種實如今正常狀況下是沒有問題的,可是卻把children是字符串(表明着文本節點)的狀況忽略了。除此以外,咱們也忽略了children是null,false,undefined等falsy值的狀況。好,咱們進一步完善一下:

function createElement(type,props,...childrens){
    const newProps = Object.assign({},props)
    const hasChildren = childrens.length > 0 
    const rawChildren = hasChildren ? [].concat(...childrens) : []
    newProps.children = rawChildren.filter(child => !!child).map(child => {
        return child instanceof Object ? child : createTextElement(child)
    })
    return {
        type,
        props:newProps
    }
}

function createTextElement(text){
    return {
        type:'TEXT_ELEMENT',
        props:{
            nodeValue:text
        }
    }
}
複製代碼

好了,有了babel的jsx編譯插件,再加上咱們實現的createElement函數,咱們如今就能夠像往常寫HTML標記同樣編寫virtual DOM對象了。

下面,咱們來總結一下。咱們寫的是:

<div id="test" onClick={() => { alert('I been clicked')}>
    我是文本節點1
    <a href="https://www.baidu.com">百度一下</a>
</div>
複製代碼

babel會將咱們的jsx轉換爲對應的javascript函數調用代碼:

createElement(
    'div',
    {
        id:test,
        onClick:() => { alert('I been clicked') }
    },
    '我是文本節點',
    createElement(
        'a',
        {
            href:'https://www.baidu.com'
        },
        '百度一下'
    )
    )
複製代碼

而在createElement函數的內部實現裏面,又會針對字符串類型的children調用createTextElement來得到對應的textElement。

最後,咱們把已實現的函數和jsx語法結合起來,一塊兒看看完整的寫法和代碼脈絡:

//  jsx的寫法
const divElement = (
    <div id="test" onClick={() => { alert('I been clicked')}>
        我是文本節點1
        <a href="https://www.baidu.com">百度一下</a>
    </div>
)

function render(){/* 內部實現,已給出 */}
function createElement(){/* 內部實現,已給出 */}
function createTextElement(){/* 內部實現,已給出 */}

window.onload = () => {
    render(divElement,document.body)
}

複製代碼

到這裏,virtual DOM -> real DOM映射的簡單實現也完成了,省時省力的jsx語法也「發明」了。那麼下一步,咱們就來談談整樹映射過程當中協調的實現。

下篇:按部就班DIY一個react(三)

相關文章
相關標籤/搜索