承接上文,假如我給你一個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語法也「發明」了。那麼下一步,咱們就來談談整樹映射過程當中協調的實現。