按部就班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(三)

相关文章
相关标签/搜索