學習一個庫的最好的方法就是實現一個, 注: 實際react的代碼可能相去甚遠。javascript
react組件能夠徹底不用JSX, 用純js來寫。 JSX語法通過babel轉化就是純js代碼, 譬如:css
const hw = <div>Hello World</div> const hw = React.createElement('div', null, "Hello World")
這兩種是等效的。 babel 經過babylon 來把JSX轉化爲js
配置以下(transform-react-jsx):html
{ "presets": [ "es2015" ], "plugins": [ ["transform-react-jsx", { "pragma": "createElement" // default pragma is React.createElement }] ] }
因此對於react庫自己的, 是不須要關心jsx語法的java
react 中virtual-dom的概念, 使用一個 js的結構vnode來描述DOM 節點。 而後, 從vnode渲染出DOM樹。
這個 vnode由3個屬性描述:nodeName(div, Son...), props, children(vnode 組成的數組), 因此 createElement的最簡實現node
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裏面書寫下面的組件的時候react
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 -> 渲染<div>am</div> -> 渲染GrandText。
顯然這是一個遞歸的過程:遞歸的停止條件是 渲染html標籤。git
暫時先不考慮 dom操做, 只考慮這個遞歸方法, 代碼以下:github
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演示地址)):json
{ "nodeName": "div", "props": {}, "children": ["i", {"nodeName": "div", "props": {}, "children": ["am"]}, { "nodeName": "div", "props": {}, "children": ["grandson"] }] }
加入實際DOM操做, 代碼以下:redux
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演示地址)
總結一下:
render 方法負責根據生成的vnode, 渲染到實際的dom的一個遞歸方法 (因爲組件 最終必定會render html的標籤。 因此這個遞歸必定是可以正常返回的)
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 替換一下。
因此
這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