從0實現一個tiny react(一)

從0實現一個tiny react(一)

注: 首發於segmentfault, 現遷移到掘金。。。javascript

學習一個庫的最好的方法就是實現一個, 實際react的代碼可能相去甚遠。css

支持JSX

react組件能夠徹底不用JSX, 用純js來寫。 JSX語法通過babel轉化就是純js代碼, 譬如:html

const hw = <div>Hello World</div>

const hw = React.createElement('div', null, "Hello World")
複製代碼

這兩種是等效的。 babel 經過babylon 來把JSX轉化爲js 配置以下(transform-react-jsx):java

{
  "presets": [
    "es2015"
  ],
  "plugins": [
    ["transform-react-jsx", {
      "pragma":  "createElement" // default pragma is React.createElement
    }]
  ]
}
複製代碼

因此對於react庫自己的, 是不須要關心jsx語法的node

渲染

react 中virtual-dom的概念, 使用一個 js的結構vnode來描述DOM 節點。 而後, 從vnode渲染出DOM樹。 這個 vnode由3個屬性描述:nodeName(div, Son...), props, children(vnode 組成的數組), 因此 createElement的最簡實現react

function createElement(comp, props, ...args) {
    let children = []
    for(let i = 0; i< args.length;i++){
        if(args[i] instanceof Array) {
            children = children.concat(args[i])
        } else {
            children.push(args[i])
        }
    }
    return {
        nodeName: comp,
        props: props || {},
        children
    }
}
複製代碼

從vnode 怎麼渲染到dom? 先想一下咱們在react裏面書寫下面的組件的時候git

class Father extends Component {
    render() {
        return (<Son/>) // React.createElement(Son) --> {nodeName: Son, props:{}, children:[]}
    }
}

class Son extends Component {
    render() {
        return (<Grandson/>) // React.createElement(Grandson) --> {nodeName: Grandson, props:{}, children:[]}
    }
}

/** *React.createElement( * "div", * null, * "i", * React.createElement( * "div", * null, * "am" * ), * React.createElement(GrandText, null) * ); */
class Grandson extends Component { 
    render() {
        return (
            <div> i <div>am</div> <GrandText/> </div>
        ) 
    }
}

class GrandText extends Component {
    render() {
        return (
            <div>grandson</div> // React.createElement(Grandson) 
        )
    }
}


render(<Father/>, document.getElementById('root'))
複製代碼

在react裏, 最終渲染出來的就是一個i am grandson。 渲染的過程就是: 渲染Father的Vnode -> 渲染Son的Vnode -> 渲染Grandson的Vnode -> 渲染div -> 渲染i -> 渲染am -> 渲染GrandText。 顯然這是一個遞歸的過程:遞歸的停止條件是 渲染html標籤。github

  1. 當 nodeName 是 html標籤, 直接操做dom
  2. 當 nodeName 是 react組件 遞歸操做 組件render返回的vnode

暫時先不考慮 dom操做, 只考慮這個遞歸方法, 代碼以下:json

function renderVDOM(vnode) {
    if(typeof vnode == "string") { // 字符串 "i an grandson"
        return vnode
    } else if(typeof vnode.nodeName == "string") {
        let result = {
            nodeName: vnode.nodeName,
            props: vnode.props,
            children: []
        }
        for(let i = 0; i < vnode.children.length; i++) {   
            result.children.push(renderVDOM(vnode.children[i]))
        }
        return result
    } else if (typeof vnode.nodeName == "function") { // 若是是function
        let func = vnode.nodeName
        let inst = new func(vnode.props)
        let innerVnode = inst.render()
        return renderVDOM(innerVnode)
    }

複製代碼

執行上面的結構將返回 (jsfiddle演示地址)):redux

{
    "nodeName": "div",
    "props": {},
    "children": ["i", {"nodeName": "div", "props": {}, "children": ["am"]}, {
        "nodeName": "div",
        "props": {},
        "children": ["grandson"]
    }]
}
複製代碼

加入實際DOM操做, 代碼以下:

function render(vnode, parent) {
    let dom
    if(typeof vnode == "string") {
        dom = document.createTextNode(vnode)
        parent.appendChild(dom)
    } else if(typeof vnode.nodeName == "string") {
        dom = document.createElement(vnode.nodeName)
        setAttrs(dom, vnode.props)
        parent.appendChild(dom)

        for(let i = 0; i < vnode.children.length; i++) {
            render(vnode.children[i], dom)
        }
    } else if (typeof vnode.nodeName == "function") {
        let func = vnode.nodeName
        
        let inst = new func(vnode.props)
        let innerVnode = inst.render()
        render(innerVnode, parent)
    }
}
function setAttrs(dom, props) {
    const allKeys = Object.keys(props)
    allKeys.forEach(k => {
        const v = props[k]

        if(k == "className") {
            dom.setAttribute("class", v)
            return
        }

        if(k == "style") {
            if(typeof v == "string") {
                dom.style.cssText = v
            }

            if(typeof v == "object") {
                for (let i in v) {
                    dom.style[i] =  v[i]
                }
            }
            return

        }

        if(k[0] == "o" && k[1] == "n") {
            const capture = (k.indexOf("Capture") != -1)
            dom.addEventListener(k.substring(2).toLowerCase(), v, capture)
            return
        }

        dom.setAttribute(k, v)
    })
}
複製代碼

渲染實際Hello World(jsfiddle演示地址) 總結一下:

  1. createElement 方法負責建立 vnode
  2. render 方法負責根據生成的vnode, 渲染到實際的dom的一個遞歸方法 (因爲組件 最終必定會render html的標籤。 因此這個遞歸必定是可以正常返回的)
    • vnode是字符串的是, 建立textNode節點
    • 當vnode.nodeName是 字符串的時候, 建立dom節點, 根據props設置節點屬性, 遍歷render children
    • 當vnode.nodeName是 function的時候, 獲取render方法的返回值 vnode', 執行render(vnode')

props 和 state

v = f(props, state)。 組件的渲染結果由 render方法, props, state共同決定,以前只是討論了render, 如今引入 props, state。

對於props, 父組件傳遞過來, 不可變。 設置到屬性上面。 由基類Component 設置props

class Component {
    constructor(props) {
        this.props = props
    }
}
複製代碼

對於 state, 在組件的生命期內是能夠修改的,當調用組件的setState方法的時候, 其實就是從新渲染 用一個新DOM樹替換老的DOM: parent.replaceChild (newdom, olddom ), 好比當我在 GrandText 上調用setState。 就是父div 把GrandText渲染出來的dom 替換一下。
因此

  1. 組件實例 必須有機制獲取到 olddom
  2. 同時 render方法的第二個參數是 parent。 組件實例必須有機制獲取到 parentDOM 這2個問題實際上是一個問題。 parent = olddom.parentNode, 因此 olddom.parentNode.replaceChild (newdom, olddom ) 。 如今的關鍵就是獲取到olddom, 這裏採用的機制是 每一個組件實例 記住 直接渲染出的組件實例/DOM(經過__rendered屬性)。 下圖:
    實例引用關係
    代碼實現:
function render (vnode, parent, comp) {
    let dom
    if(typeof vnode == "string") {
        const dom = ...  // 建立文本節點
        comp && (comp.__rendered = dom)
        ...  // other op
    } else if(typeof vnode.nodeName == "string") {
        const dom = ... // 建立 dom節點
        comp && (comp.__rendered = dom)
        ... // other op
    } else if (typeof vnode.nodeName == "function") {
        const inst = ... // 建立 組件實例
        comp && (comp.__rendered = inst)
        ... // other op
    }
}
複製代碼

其中 comp 參數表明 "我是被誰渲染的"。 獲取olddom的代碼實現:

function getDOM(comp) {
    let rendered = comp.__rendered
    while (rendered instanceof Component) { //判斷對象是不是dom
        rendered = rendered.__rendered
    }
    return rendered
}
複製代碼

調用 setState 使用olddom替換老的dom 代碼以下:

function render(vnode, parent, comp, olddom) {
    let dom
    if(typeof vnode == "string") {
        ...
        if(olddom) {
            parent.replaceChild(dom, olddom)
        } else {
            parent.appendChild(dom)
        }
        ...
    } else if(typeof vnode.nodeName == "string") {
        ...
        if(olddom) {
            parent.replaceChild(dom, olddom)
        } else {
            parent.appendChild(dom)
        }
        ...
    } else if (typeof vnode.nodeName == "function") {
        ...
        render(innerVnode, parent, inst, olddom)
    }
}
複製代碼

拼湊一下以上功能, 完整代碼實現:

///Component
class Component {
    constructor(props) {
        this.props = props
    }

    setState(state) {
        setTimeout(() => {
            this.state = state
            const vnode = this.render()
            let olddom = getDOM(this)
            render(vnode, olddom.parentNode, this, olddom)
        }, 0)
    }
}


function getDOM(comp) {
    let rendered = comp.__rendered
    while (rendered instanceof Component) { //判斷對象是不是dom
        rendered = rendered.__rendered
    }
    return rendered
}

///render
function render (vnode, parent, comp, olddom) {
    let dom
    if(typeof vnode == "string" || typeof vnode == "number") {
        dom = document.createTextNode(vnode)
        comp && (comp.__rendered = dom)
        parent.appendChild(dom)

        if(olddom) {
            parent.replaceChild(dom, olddom)
        } else {
            parent.appendChild(dom)
        }
    } else if(typeof vnode.nodeName == "string") {
        dom = document.createElement(vnode.nodeName)

        comp && (comp.__rendered = dom)
        setAttrs(dom, vnode.props)

        if(olddom) {
            parent.replaceChild(dom, olddom)
        } else {
            parent.appendChild(dom)
        }

        for(let i = 0; i < vnode.children.length; i++) {
            render(vnode.children[i], dom, null, null)
        }
    } else if (typeof vnode.nodeName == "function") {
        let func = vnode.nodeName
        let inst = new func(vnode.props)

        comp && (comp.__rendered = inst)

        let innerVnode = inst.render(inst)
        render(innerVnode, parent, inst, olddom)
    }
}
複製代碼

有狀態組件 演示地址, have fun!

總結一下: render方法負責把vnode渲染到實際的DOM, 若是組件渲染的DOM已經存在, 就替換, 而且保持一個 __rendered的引用鏈

其餘

代碼託管在github。 以爲有幫助,點個star。哈哈哈。。。 本文所講的代碼部分在 propsAndState 這個tag上:

git clone https://github.com/ykforerlang/tinyreact.git
git branch [yourbranchname] propsAndState
複製代碼

相關文章

相關文章
相關標籤/搜索