承接上文,假如我给你一个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语法也“发明”了。那么下一步,咱们就来谈谈整树映射过程当中协调的实现。