手写react源码 react vdom的实现

我们知道react的jsx语法最终会被babel转译成 React.createElement语法,通过这个语法,react节点就会被编译成一棵vdom树结构,我们看一个例子

let ele1 = <h1 id="title">
    <span>hello</span>
    <span>world</span>
</h1>
ReactDOM.render(ele1, document.getElementById('root'))

这个语法经过baebl转义之后就是下面这个语法

React.createElement("h1", {
  id: "title"
}, /*#__PURE__*/React.createElement("span", null, "hello"), /*#__PURE__*/React.createElement("span", null, "world"));

我们可以用 React.createElement 编写:

let ele2 = React.createElement('h1',{id: 'title'}, React.createElement('span',null,'hello'), React.createElement('span',null,'world'))
console.log(ele2)
ReactDOM.render(ele2, document.getElementById('root'))

我们看一下这个ele2到底长什么样子

 

  • type表示标签名或者是你的react组件
  • props是该组件的事件和一些属性
  • children表示是子元素,可以是string,也可以是一个vdom

现在知道了vdom是啥我们就可以实现一个自己的React.createElement方法来创建vdom了

一、实现React.createElement来构建节点的vdom

function createElement(type,config,children){
    let props = {}
    //把config里面的所有属性都拷贝到props里面
    for(let key in config) {
        props[key] = config[key]
    }
    //获取子节点的个数
    debugger
    const childrenLength = arguments.length - 2
    if(childrenLength === 1){ //只有一个儿子,props.children是一个对象,
        props.children = children
    }else if(childrenLength > 1) {
        //如果儿子数量大于1个的话,就把所有儿子都放到一个数组里
        props.children = Array.prototype.slice.call(arguments,2)
    }
     return {type,props} //type 表示react元素类型 string number Function Class
}
export default {createElement} 

对照上面写的React.createElement的例子来实现createElement的代码。

type表示传入的元素类型,string number、function、Class ,对照上面的就是'h1',config表示传入的属性,id className等,把config里面的所有属性都拷贝到props里面,children表示子节点,也需要拷贝到props里面,具体不知道有几个子节点但是可以通过计算参数数量得出,只有一个儿子的情况下,props.children 是一个对象的形式,如果子节点个数大于1,就需要以数组的形式来表示,最后返回type和props,来看打印结果:

 

 可以看到跟源码的打印结果一摸一样

二、实现 ReactDOM.render来渲染节点到页面

function render(node,parent ){
    //node react 节点, parant 父容器,是一个真实的dom元素
    //1.首先要拿到节点的type 和 props
    let type, props
    type = node.type //h1, function ClassComponent
    props = node.props
    //2.根据虚拟dom创建真实dom插入到父容器
    let domElement = document.createElement(type) //创建真实dom
    parent.appendChild(domElement) //插入到父容器
}
export default {
    render
}

对照下面的渲染案例我们来分析一下:

ReactDOM.render(ele2, document.getElementById('root'))

render函数需要两个参数,一个是需要要渲染的node节点,一个是父容器,那么我们如何获取到node节点呢?根据我们上面的实现,我们可以得到一个节点的vdom树的结构,就可以获取到它的type和props,type表示的是一个节点的元素类型,h1、div、span、Function、ClassComponent等,所以就可以根据这个type来创建一个真实dom插入到父容器中。我们可以看看现在的渲染结果:

可以看到,现在h1已经插入到了父容器里面,但是儿子节点和其他属性还没有处理,我们接下来处理儿子节点

function render(node,parent ){
    //node react 节点, parant 父容器,是一个真实的dom元素
    //1.首先要拿到节点的type 和 props
    let type, props
    type = node.type //h1, function ClassComponent
    props = node.props
    //2.根据虚拟dom创建真实dom插入到父容器
    let domElement = document.createElement(type) //创建真实dom
    
    // 处理属性和儿子节点
    for (let propName in props) {
        if(propName === 'children') {
            let children = props.children //儿子节点可能是个对象也可能时个数组
            //如果不是数组就要转成数组
            if (!Array.isArray(children)) {
                children = [children]
            }
            //递归创建儿子节点
            children.forEach(child => {
                render(child, domElement) //将儿子节点插入到自己的容器里面
            });
        }
    }
    parent.appendChild(domElement) //插入到父容器
}
export default {
    render
}

通过递归的方式来创建儿子节点,看看渲染效果

 可以看到儿子节点也被插入到了页面中,但是我们的文字哪里去了?根据我们前面的打印结果可知,文字也是儿子节点,只是string类型,所以我们需要判断一下,如果是string类型的就创建一个文本节点插入到页面中。

function render(node,parent ){
    //node react 节点, parant 父容器,是一个真实的dom元素
    //如果节点是个字符串,就创建一个文本节点插入到页面上
    if(typeof node === 'string'){
        return parent.appendChild(document.createTextNode(node)) 
    }
   
    //1.首先要拿到节点的type 和 props
    let type, props
    type = node.type //h1, function ClassComponent
    props = node.props
    //2.根据虚拟dom创建真实dom插入到父容器
    let domElement = document.createElement(type) //创建真实dom
    
    // 处理属性和儿子节点
    for (let propName in props) {
        if(propName === 'children') {
            let children = props.children //儿子节点可能是个对象也可能时个数组
            //如果不是数组就要转成数组
            if (!Array.isArray(children)) {
                children = [children]
            }
            //递归创建儿子节点
            children.forEach(child => {
                render(child, domElement) //将儿子节点插入到自己的容器里面
            });
        }
    }
    parent.appendChild(domElement) //插入到父容器
}
export default {
    render
}

再看渲染结果

儿子节点全部渲染成功

接下来需要渲染id className style 等属性了

function render(node,parent ){
    //node react 节点, parant 父容器,是一个真实的dom元素
    //如果节点是个字符串,就创建一个文本节点插入到页面上
    if(typeof node === 'string'){
        return parent.appendChild(document.createTextNode(node)) 
    }
    
    //1.首先要拿到节点的type 和 props
    let type, props
    type = node.type //h1, function ClassComponent
    props = node.props
    //2.根据虚拟dom创建真实dom插入到父容器
    let domElement = document.createElement(type) //创建真实dom
    
    // 处理属性和儿子节点
    for (let propName in props) {
        if(propName === 'children') {
            let children = props.children //儿子节点可能是个对象也可能时个数组
            //如果不是数组就要转成数组,方便迭代
            if (!Array.isArray(children)) {
                children = [children]
            }
            //递归创建儿子节点
            children.forEach(child => {
                render(child, domElement) //将儿子节点插入到自己的容器里面
            });
        }else if(propName === 'className') { //处理类名
            domElement.className = props.className
        }else if (propName === 'style') { //处理style 值就是一个行内样式对象
             let styleObj = props.style //{color: 'red',backgroundColor:'yellow'}
             for(let attr in styleObj) {
                 domElement.style[attr] = styleObj[attr]
             }
        }else { //处理id
            domElement.setAttribute(propName,props[propName])
        }
    }
    parent.appendChild(domElement) //插入到父容器
}
export default {
    render
}

我们可以打印一下vdom结构方便我们理解上面的代码

可以看看渲染结果

 接下来实现组件渲染

我们首先看一下组件的vdom结构

function Welcome(props) {
  return (
    <h1 id={props.id}>
      <span>hello</span>
      <span>world</span>
    </h1>
  );
}
let ele3 = React.createElement(Welcome,{id:'title'})
console.log(ele3)

可以看到,type表示他是一个函数式组件 

三、函数组件的渲染

接下来就可以写组件的渲染了

function render(node,parent ){
    //node react 节点, parant 父容器,是一个真实的dom元素
    //如果节点是个字符串,就创建一个文本节点插入到页面上
    if(typeof node === 'string'){
        return parent.appendChild(document.createTextNode(node)) 
    }  
    
    //1.首先要拿到节点的type 和 props
    let type, props
    type = node.type //h1, function ClassComponent
    props = node.props
    //3.处理组件的渲染
    if (typeof type === 'function'){ //函数组件
        let element = type(props)
        type = element.type
        props = element.props
    }
    //2.根据虚拟dom创建真实dom插入到父容器
    let domElement = document.createElement(type) //创建真实dom
    
    // 处理属性和儿子节点
    for (let propName in props) {
        if(propName === 'children') {
            let children = props.children //儿子节点可能是个对象也可能时个数组
            //如果不是数组就要转成数组
            if (!Array.isArray(children)) {
                children = [children]
            }
            //递归创建儿子节点
            children.forEach(child => {
                render(child, domElement) //将儿子节点插入到自己的容器里面
            });
        }else if(propName === 'className') { //处理类名
            domElement.className = props.className
        }else if (propName === 'style') { //处理style 值就是一个行内样式对象
             let styleObj = props.style //{color: 'red',backgroundColor:'yellow'}
             for(let attr in styleObj) {
                 domElement.style[attr] = styleObj[attr]
             }
        }else { //处理id
            domElement.setAttribute(propName,props[propName])
        }
    }
    
    parent.appendChild(domElement) //插入到父容器
}
export default {
    render
}

通过typeof判断它的类型,如果是函数就去执行这个函数,并且用一个参数去接收它的返回值,再去拿到它的type和props属性进行渲染

看看它的渲染结果

不太明白__self 和__source是是啥?

接下来开始写类组件的构建和渲染过程

四、类组件的构建和渲染

先来回忆一下类组件的写法

welcome extends React.Component{
    constructor(props){
        super(props)
    }
    render() {
        return (
            <h1 id={this.props.id}>
              <span>hello</span>
              <span>world</span>
            </h1>
          );
    }
}
let ele3 = React.createElement(Welcome,{id:'title'})
console.log(ele3)

可以看到所有的类组件都是继承于父类的,所以在类组件渲染之前要先创建一个父类,让所有的子类继承这个父类,另外我们知道,class类不能直接运行,必须要通过new关键字才能执行,所以在渲染的时候需要判断这个组件是类组件还是函数组件代码如下:

首先要在react/createElement中定义一个父类

//定义一个父类,让子组件继承这个父类
class Component {
    constructor(props) {
        props = this.props
    }
    static isReactComponent = true
}
function createElement(type,config,children){
    let props = {}
    //把config里面的所有属性都拷贝到props里面
    // debugger
    for(let key in config) {
        props[key] = config[key]
    }
    //获取子节点的个数
    
    const childrenLength = arguments.length - 2
    if(childrenLength === 1){ //只有一个儿子,props.children是一个对象,
        props.children = children
    }else if(childrenLength > 1) {
        //如果儿子数量大于1个的话,就把所有儿子都放到一个数组里
        props.children = Array.prototype.slice.call(arguments,2)
    }
     return {type,props} //type 表示react元素类型 string number Function Class
}
export default {createElement,Component} 

看看vdom的构建结果,

接着在渲染方法render中开始渲染

function render(node,parent ){
    //node react 节点, parant 父容器,是一个真实的dom元素
    //如果节点是个字符串,就创建一个文本节点插入到页面上
    if(typeof node === 'string'){
        return parent.appendChild(document.createTextNode(node)) 
    }  
    
    //1.首先要拿到节点的type 和 props
    let type, props
    type = node.type //h1, function ClassComponent
    props = node.props
    //3.处理组件的渲染
    if(type.isReactComponent){ //类组件
        let element = new type(props).render() //创建实例调用它的render方法
        type = element.type
        props = element.props
    }else if (typeof type === 'function'){ //函数组件
        let element = type(props)
        type = element.type
        props = element.props
    }
    //2.根据虚拟dom创建真实dom插入到父容器
    let domElement = document.createElement(type) //创建真实dom
    
    // 处理属性和儿子节点
    for (let propName in props) {
        if(propName === 'children') {
            let children = props.children //儿子节点可能是个对象也可能时个数组
            //如果不是数组就要转成数组
            if (!Array.isArray(children)) {
                children = [children]
            }
            //递归创建儿子节点
            children.forEach(child => {
                render(child, domElement) //将儿子节点插入到自己的容器里面
            });
        }else if(propName === 'className') { //处理类名
            domElement.className = props.className
        }else if (propName === 'style') { //处理style 值就是一个行内样式对象
             let styleObj = props.style //{color: 'red',backgroundColor:'yellow'}
             for(let attr in styleObj) {
                 domElement.style[attr] = styleObj[attr]
             }
        }else { //处理id
            domElement.setAttribute(propName,props[propName])
        }
    }
    
    parent.appendChild(domElement) //插入到父容器
}
export default {
    render
}

至此,react vdom的构建和渲染过程就全部结束了

猜你喜欢

转载自www.cnblogs.com/lyt0207/p/12672475.html