菜鸡是怎么手写 React 之 实现 setState

1,220 阅读1分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

上篇文章讲述了如何将vdom、函数组件、类组件转换为真实的dom节点,这篇文章主要讲述如何去实现数据的更新,也就是实现setState方法

页面代码

首先把下面的代码全部复制到js文件中,让我们的页面变成一个可删除、可增加的列表

class Component {
    constructor(props) {
        this.props = props || {};
        this.state = null;
    }
    setState(nextState) {
    }
}

function Item(props) {
    return <li className="item" style={props.style}>{props.children} <a href="#" onClick={props.onRemoveItem}>X </a></li>;
}

class List extends Component {
    constructor(props) {
    super();
    this.state = {
        list: [
            {
                text: 'aaa',
                color: 'pink'
            },
            {
                text: 'bbb',
                color: 'orange'
            },
            {
                text: 'ccc',
                color: 'yellow'
            }
        ]
    }
}
handleItemRemove(index) {
    this.setState({
    list: this.state.list.filter((item, i) => i !== index)
    });
}
handleAdd() {
    this.setState({
    list: [
        ...this.state.list,
        {
        text: document.getElementById('myRef').value
        }
    ]
    });
}
render() {

    return <div>
        <ul className="list">
            {this.state.list.map((item, index) => {
            return <Item key={`item${index}`} style={{ background: item.color, color: this.state.textColor}} onRemoveItem={() => this.handleItemRemove(index)}>{item.text}</Item>
            })}
        </ul>
        <div>
            <input id='myRef'/>
            <button onClick={this.handleAdd.bind(this)}>add</button>
        </div>
    </div>;
    }
}

const isTextNode = (vdom) => { return typeof vdom === 'string' || typeof vdom === 'number' }

const isElementNode = (vdom) => { return typeof vdom === 'object' && typeof vdom.type === 'string' }

const isEventListenerAttr = (key,value) => { return typeof value === 'function' && key.startsWith('on')}

const isStyleAttr = (key,value) => { return key === 'style' && typeof value === 'object'}

const isPlainAttr = (value) => { return typeof value !== 'object' && typeof value !== 'function'}

const isComponentVdom = (vdom) => { return typeof vdom.type == 'function' }

const setAttribute = (dom,key,value) => {
    if(isEventListenerAttr(key,value)){//这里要设置的是元素的响应事件
        const eventType = key.slice(2).toLowerCase()
        dom.addEventListener(eventType,value)
    }else if(key === 'className'){
        //className属性需要特殊设置,设置后变成<div class='xxx'></div>,而用dom.setAttribute会变成<div className='xxx'></div>
        dom[key] = value
    }else if(isStyleAttr(key,value)){//style属性是对象,需要特别设置
        Object.assign(dom.style,value)
    }else if(isPlainAttr(value)){//其他常规的属性在这里设置,比如id属性、title属性等
        dom.setAttribute(key,value)
    }
}
function renderComponent(vdom,parent){
    if (Component.isPrototypeOf(vdom.type)) {
        const instance = new vdom.type(vdom.props);
        const componentVdom = instance.render();
        instance.dom = render(componentVdom, parent);
        return instance.dom
    } else {
        //执行type属性上的函数,并将vdom.props属性传入
        const componentVdom = vdom.type(vdom.props);
        //最后执行render函数
        return render(componentVdom, parent);
    }
}

const render = (vdom,parent) => {
    const mount = parent ? (el => parent.appendChild(el)) : (el => el);
    if(isTextNode(vdom)){
        return mount(document.createTextNode(vdom))
    }else if(isElementNode(vdom)){
        const newDom = document.createElement(vdom.type)
    //开始设置节点的属性
    for(let props in vdom.props){
        if(props !== 'children'){//需要将children过滤掉
        setAttribute(newDom,props,vdom.props[props])
        }
    }
    //对元素的子节点进行递归
    if(vdom.props.children){
        for (const child of vdom.props.children) {
            render(child, newDom);
        }
    }
    return mount(newDom)
    }else if (isComponentVdom(vdom)) {//当我们判断到传入的vdom是由函数组件编译而来的时候
        return renderComponent(vdom,parent)
    }
}
render(<List textColor={'pink'}/>,document.documentElement)

现在页面是这样的

originPage.jpg 当我们在input框输入值时,会触发handleAdd方法

handleAdd() {
    this.setState({
        list: [
            ...this.state.list, 
            {
                text: document.getElementById('myRef').value
            }
        ]
    });
}

而现在setState方法是空的,最简单粗暴的方法就是,当我们去更改了类组件中的state的值后,我们用这一份新的state,重新调用类组件的render方法,重新生成一份vdom,再用这个新的vdom,去调用全局的render方法,将旧的dom节点全部删掉,再将新的dom节点全部挂载上去

setState(nextState) {
    //将state的值进行更新
    this.state = Object.assign(this.state,nextState);
    //当触发this.render时,会通过state的值去生成vdom,而state的值我们是已经更新过的,所以这一份vdom是新的vdom
    const newDom = render(this.render());
    //进行节点的替换
    this.dom.replaceWith(newDom);
    //需要将dom节点的引用进行更改
    this.dom = newDom;
 }

效果会是这样的

222.gif 但这样的页面性能会很差,所以我们需要通过patch函数去找出可以复用的dom节点,也就是我们常常说的diff算法,我们先来看一看diff算法的理论知识

diff算法理论知识

  • 首先我们要清楚的一点是,diff算法比较的对象是谁,diff算法是旧的dom节点和新的vdom节点之间的比较,而不是旧的vdom节点和新的vdom节点去进行对比

    • 1678072510195.jpg
  • 其次,我们常常会看到这样一句话,找出尽可能可以复用的dom节点,实际上,我们在对比旧的dom节点和新的vdom节点后,我们会直接在旧的dom节点上面更改,那么,没有更改到的dom节点我们就说它是可以复用的dom节点,而不是说将没有更改过的dom节点重新挂载到根节点上面去

  • 我们通过一张图来走一遍diff算法

    • WechatIMG6.jpeg

    • 实diff算法会经过两轮比较

      • 第一轮,会进行同层次、同位置之间的比较,就是小a=》大A,小b=》大B,小c=》大C,小d=》大E,这样去对比,因为是递归的关系,所以如果小b节点还有子节点,会进行一个深度遍历

      • 在经过第一轮比较后,我们会发现,旧的dom节点中,a,b,c在经过比较后,都是可以复用的节点,现在旧的dom节点中只剩下d节点未被复用,但实际上,d节点也是应该被复用的,只不过是因为它后移了一位,导致它没有被识别到应该被复用,所以如果还会进行第二次比较

        • 我们会将旧的dom节点中,还未被复用过的节点,全部放入一个map结构中,然后去遍历一遍新的vdom,vdom节点中,还未被打上标签的对象(如果已经找到可以复用的会打上标签),会在map结构中去寻找是否有可以复用的dom节点,找到了会给自己打上标签,同时将该dom节点从map结构中删除
  • 从上面讲述的流出可以看出,diff算法对比,其实也就是一个递归的操作,那一个是dom结构,另外一个是对象,我们应该怎么进行对比操作?我在下边列举了一小部分代码,并且添加上了对应的注释,可以先把这里理解了再往下看

    //如果dom节点的nodeName和vdom的type属性不一致,那么他们就不是同一个节点,可以直接进行替换
    if (dom.nodeName !== vdom.type.toUpperCase() && typeof vdom === 'object') {
        //替换操作
        return replace(render(vdom, parent));
    } else{//上边的图片中,dom的nodeName是div,vdom的type也是div,所以他们是同一个节点,可以进行子节点的比较
        //将dom节点的childNodes放入oldDomsArr
        const oldDomsArr = [].concat(...dom.childNodes)
        //对vdom.children进行遍历
        [].concat(...vdom.children).map((child, index) => {
            通过index,进行同层级,同位置之间的对比
            patch(oldDoms[index], child);
        });
    }
    

setState的具体实现

  • 现在的setState函数是这样的
    setState(nextState) {
         this.state = Object.assign(this.state,nextState);//生成新的state
        //判断state和props时候和之前存在不一样,避免不必要的渲染
        if(this.dom && this.shouldComponentUpdate(this.props, nextState)) {
            //旧的dom节点和新的vdom节点的对比
            //dom属性会在render函数中,渲染类组件时挂载到dom节点上面去
            patch(this.dom, this.render());
        }
    }
    
    shouldComponentUpdate(nextProps, nextState) {
        return nextProps != this.props || nextState != this.state;
    }
    
  • 进入到patch函数中要注意的参数有dom节点和vdom对象,这两个对象分别都有两种情况,dom节点可能是一个文本节点也有可能是一个元素节点,同样,vdom有可能是一个对象,也有可能是一个字符串或者是数字,所以我们先将上面说的几种情况全部列举出来
     function patch(dom,vdom,parent = dom.parentNode){
         //后续有多处需要进行替换节点操作,所以把replaceChild函数封装起来
        const replace = parent ? el => {
            parent.replaceChild(el, dom);
            return el;
         } : (el => el);
        if(dom instanceof Text){//dom节点只有两大类,文本节点和元素节点,我们先对dom节点做一个区分
             if (typeof vdom === 'object') {
                    
              } else {
                   
              }
        }else{//表示都是元素节点
            if(typeof vdom === 'object'){//元素节点有两种情况,传进来的vdom是一个对象,或者是一个数字或字符串
                
            }else if(typeof vdom === 'string' || typeof vdom === 'number'){//文本节点
            
            }
        }
    }
    

dom是文本节点的情况

  • 如果vdom是个元素节点,那么这两个节点肯定不能复用,那么直接将vdom的转变成dom再替换上去
  • 如果vdom也是一个文本节点,那么我们去对比他们的文本是否相同,如果不相同我们再将vdom转变成dom再替换上去,如果相同我们不做任何处理
    if (dom instanceof Text) {
        if (typeof vdom === 'object') {
            return replace(render(vdom, parent));
        } else {
            return dom.textContent != vdom ? replace(render(vdom, parent)) : dom;
        }
    }
    

dom是元素节点的情况

vdom是一个数字或是一个字符串的情况

  • 先来看比较简单的情况,如果vdom是一个数字或是一个字符串,表示它是一个文本节点,但是dom现在是元素节点,所以这个dom节点就不能复用,直接替换
    if(typeof vdom === 'object'){//元素节点有两种情况,传进来的vdom是一个对象,或者是一个数字或字符串
    
    }else if(typeof vdom === 'string' || typeof vdom === 'number'){//文本节点
        return replace(render(vdom, dom));
    }
    

vdom是一个对象的情况

  • 如果vdom是一个对象,那么它还会有很多种情况,因为这个vdom它有可能是由类组件转换而来,也有可能是由函数组件转换而来,也有可能就是一个普通的vdom,所以我们还需要对vdom.type属性以及该组件是类组件还是函数组件进行一个判断
    if(typeof vdom === 'object'){//元素节点有两种情况,传进来的vdom是一个对象,或者是一个数字或字符串
            if(typeof vdom.type === 'function'){//vdom可能是由类组件或是函数组件转换而来
                 if(Component.isPrototypeOf(vdom.type)){//类组件
    
                 }else{//函数组件
    
                 }
            }else if(typeof vdom.type === 'string'){//一个普通的vdom
    
            }
    }
    
vdom是类组件的情况
  • 对于vdom是类组件转化过来的情况,它要对比的dom,有可能是同一个组件渲染的,也有可能不是同一个组件渲染的,所以我们需要知道,当前的dom,是由哪一个类组件转换而来的;
  • 我们只要改一下render函数的代码,让它在渲染类组件的时候打上一个标记,让这个dom节点和我们的类函数挂上关系,我们通过判断这个关系就可以知道是否是同一个组件渲染出来的
  • 对于相同的组件,因为此时我们只是判断了他们是由相同的组件渲染而来,但我们还不知道它们的子节点是否完全一致,所以我们需要把更新好的props传入this.render()方法中去,生成新的vdom,再用这个vdom和dom节点进行patch比较(重点!!!)
  • 如果不是同一个组件渲染出来的,我们直接用vdom的值生成一份新的dom节点,替换掉旧节点就行
    //render函数中渲染组件的代码新增_instance指针
    if (Component.isPrototypeOf(vdom.type)) {
            
            instance.dom_instance = instance;
            
    } 
    
    if(Component.isPrototypeOf(vdom.type)){//类组件
        //dom节点上挂载_instance属性,指向的是一个对象,这个对象是class类的实例,这个对象的constructor指向生成它的函数
        if(dom.__instance && dom.__instance.constructor == vdom.type){
            //我们把实例上的props属性进行更新,将新的props传递进去
            dom.__instance.props = props;
            //dom.__instance.render()会生成一个新的vdom,因为dom.__instance的props属性已经更新了
            return patch(dom, dom.__instance.render(), parent);
        }else{//不是同一个组件渲染出来的直接调用renderComponent生成新的dom节点
            //renderComponent是render函数中处理组件的逻辑
            const componentDom = renderComponent(vdom, parent);
            if (parent){
                parent.replaceChild(componentDom, dom);
                return componentDom;
            } else {
                return componentDom
            }
        }
    }else{//函数组件
    
    }
    
vdom是函数组件的情况
  • 同样的,对于函数组件,我们也可以在渲染这个组件的时候,打上标签
    if (Component.isPrototypeOf(vdom.type)) {//渲染类组件
    
    } else {//渲染函数组件
        //执行type属性上的函数,并将vdom.props属性传入
        const componentVdom = vdom.type(vdom.props);
        const functionDom = render(componentVdom, parent);
        functionDom._fcInstance = vdom.type;//打上标签
        //最后执行render函数
        return functionDom
    }
    
    if(Component.isPrototypeOf(vdom.type)){//类组件
    
    }else{//函数组件
        if(dom._fcInstance && dom._fcInstance === vdom.type){
            //如果是同一个函数渲染出来的,我们将props传入vdom.type函数中去生成一个新的vdom
            return patch(dom, vdom.type(props), parent);
        }else{
            const componentDom = renderComponent(vdom, parent);
            if (parent){
                parent.replaceChild(componentDom, dom);
                return componentDom;
            } else {
                return componentDom
            }
        }
    }
    
vdom是一个普通的对象的情况
  • 如果vdom是一个普通的对象,那么这时候,就和diff理论知识中举的例子一样,dom和vdom会进行真正的比较,vdom不会再去进行转化。

    else if(typeof vdom.type === 'string'){//一个普通的vdom
        if (dom.nodeName !== vdom.type.toUpperCase()) {//如果dom节点和vdom的type不能对应起来,比如div对p,那么就直接替换整个节点
            return replace(render(vdom, parent));
        } else{//如果能对应起来,比如div对div,那么就进行子节点的对比
            //dom.childNodes会获取到dom节点的子元素,并且我们需要考虑到key到情况,如果没有key,我们就赋予它一个跟下标有关的key值
            const oldDoms = {};
            [].concat(...dom.childNodes).map((child, index) => {
                const key = child.__key || `__index_${index}`;
                oldDoms[key] = child;
            });
            vdom.props.children && [].concat(...vdom.props.children).map((child, index) => {
            const key = child.props && child.props.key || `__index_${index}`;
                //关键代码是下面这一行,实际上就是两个数组进行相同位置上的元素比较,如果相同位置上有元素,那么就将这两个元素拿去patch比较,如果没有,那么就直接生成新的dom节点
                dom.appendChild(oldDoms[key] ? patch(oldDoms[key], child) : render(child, dom));
                delete oldDoms[key];
            });
            //将不用的dom节点删除,释放内存
            for (const key in oldDoms) {
                oldDoms[key].remove();
            }
            //删除dom的属性
            for (const attr of dom.attributes) dom.removeAttribute(attr.name);
            //对dom属性重新设置
            for (const prop in vdom.props) {
                if(prop !== 'children'){
                    setAttribute(dom, prop, vdom.props[prop]);
                }
            }
            return dom
        }
    }
    
    //这里是上面代码块所需要的函数
    
    //设置属性的方法
    const setAttribute = (dom, key, value) => {
        if (isEventListenerAttr(key, value)) {
            //把各种事件的 listener 放到 dom 的 __handlers 属性上,每次删掉之前的,换成新的。
            const eventType = key.slice(2).toLowerCase();
            dom.__handlers = dom.__handlers || {};
            dom.removeEventListener(eventType, dom.__handlers[eventType]);
            dom.__handlers[eventType] = value;
            dom.addEventListener(eventType, dom.__handlers[eventType]);
        } else if (key == 'checked' || key == 'value' || key == 'className') {
            dom[key] = value;
        } else if (isStyleAttr(key, value)) {
            Object.assign(dom.style, value);
        } else if (key == 'key') {
            dom.__key = value;
        } else if (isPlainAttr(key, value)) {
            dom.setAttribute(key, value);
        }
    }
    

页面效果是这样的

444.gif

总结

实现setState方法的关键在于,怎么去对比dom和vdom从而找到可以复用的dom节点,并且我们要理解diff算法的思想以及dom节点的渲染流程;

这篇文章实现的setState是递归渲染的,如果vdom树太大,计算量会非常的大,并且有可能会感到卡顿,所以现在的react是fiber架构,递归渲染更改成循环渲染,这样随时可以打断去执行优先级更高的任务。

3.0会实现react的fiber和hook功能。

页面完整代码

class Component {
    constructor(props) {
    this.props = props || {};
    this.state = null;
}
    setState(nextState) {
        this.state = Object.assign(this.state,nextState);//生成新的state
        //判断state和props时候和之前存在不一样,避免不必要的渲染
        if(this.dom && this.shouldComponentUpdate(this.props, nextState)) {
            //旧的dom节点和新的vdom节点的对比
            //dom属性会在render函数中,渲染类组件时挂载到dom节点上面去
            patch(this.dom, this.render());
        }
    }
    shouldComponentUpdate(nextProps, nextState) {
        return nextProps != this.props || nextState != this.state;
    }
}
function patch(dom,vdom,parent = dom.parentNode){
    //后续有多处需要进行替换节点操作,所以把replaceChild函数封装起来
    const replace = parent ? el => {
        parent.replaceChild(el, dom);
        return el;
    } : (el => el);
    if(dom instanceof Text){//dom节点只有两大类,文本节点和元素节点,我们先对dom节点做一个区分
        if (typeof vdom === 'object') {
            return replace(render(vdom, parent));
        } else if(typeof vdom === 'string' || typeof vdom === 'number'){
            return dom.textContent != vdom ? replace(render(vdom, parent)) : dom;
        }
    }else{//表示都是元素节点
        if(typeof vdom === 'object'){//元素节点有两种情况,传进来的vdom是一个对象,或者是一个数字或字符串
            if(typeof vdom.type === 'function'){//vdom可能是由类组件或是函数组件转换而来
                if(Component.isPrototypeOf(vdom.type)){//类组件
                    //dom节点上挂载_instance属性,指向的是一个对象,这个对象是class类的实例,这个对象的constructor指向生成它的函数
                    if(dom.__instance && dom.__instance.constructor == vdom.type){
                        //我们把实例上的props属性进行更新,将新的props传递进去
                        dom.__instance.props = props;
                        //dom.__instance.render()会生成一个新的vdom,因为dom.__instance的props属性已经更新了
                        return patch(dom, dom.__instance.render(), parent);
                    }else{//不是同一个组件渲染出来的直接调用renderComponent生成新的dom节点
                        //renderComponent是render函数中处理组件的逻辑
                        const componentDom = renderComponent(vdom, parent);
                        if (parent){
                            parent.replaceChild(componentDom, dom);
                            return componentDom;
                        } else {
                            return componentDom
                        }
                    }
                }else{//函数组件
                    if(dom._fcInstance && dom._fcInstance === vdom.type){
                        //如果是同一个函数渲染出来的,我们将props传入vdom.type函数中去生成一个新的vdom
                        return patch(dom, vdom.type(props), parent);
                    }else{
                        const componentDom = renderComponent(vdom, parent);
                        if (parent){
                            parent.replaceChild(componentDom, dom);
                            return componentDom;
                        } else {
                            return componentDom
                        }
                    }
                }
            }else if(typeof vdom.type === 'string'){//一个普通的vdom
                if (dom.nodeName !== vdom.type.toUpperCase()) {//如果dom节点和vdom的type不能对应起来,比如div对p,那么就直接替换整个节点
                    return replace(render(vdom, parent));
                } else{//如果能对应起来,比如div对div,那么就进行子节点的对比
                        //dom.childNodes会获取到dom节点的子元素,并且我们需要考虑到key到情况,如果没有key,我们就赋予它一个跟下标有关的key值
                        const oldDoms = {};
                        [].concat(...dom.childNodes).map((child, index) => {
                            const key = child.__key || `__index_${index}`;
                            oldDoms[key] = child;
                        });
                        vdom.props.children && [].concat(...vdom.props.children).map((child, index) => {
                            const key = child.props && child.props.key || `__index_${index}`;
                            //关键代码是下面这一行,实际上就是两个数组进行相同位置上的元素比较,如果相同位置上有元素,那么就将这两个元素拿去patch比较,如果没有,那么就直接生成新的dom节点
                            dom.appendChild(oldDoms[key] ? patch(oldDoms[key], child) : render(child, dom));
                            delete oldDoms[key];
                         });
                        //将不用的dom节点删除,释放内存
                        for (const key in oldDoms) {
                            oldDoms[key].remove();
                        }
                        //删除dom的属性
                        for (const attr of dom.attributes) dom.removeAttribute(attr.name);
                        //对dom属性重新设置
                        for (const prop in vdom.props) {
                            if(prop !== 'children'){
                                setAttribute(dom, prop, vdom.props[prop]);
                            }
                         }
                        return dom
                    }
                }
            }else if(typeof vdom === 'string' || typeof vdom === 'number'){//文本节点
                return replace(render(vdom, dom));
            }
        }
    }
//设置属性的方法
const setAttribute = (dom, key, value) => {
    if (isEventListenerAttr(key, value)) {
        //把各种事件的 listener 放到 dom 的 __handlers 属性上,每次删掉之前的,换成新的。
        const eventType = key.slice(2).toLowerCase();
        dom.__handlers = dom.__handlers || {};
        dom.removeEventListener(eventType, dom.__handlers[eventType]);
        dom.__handlers[eventType] = value;
        dom.addEventListener(eventType, dom.__handlers[eventType]);
    } else if (key == 'checked' || key == 'value' || key == 'className') {
        dom[key] = value;
    } else if (isStyleAttr(key, value)) {
        Object.assign(dom.style, value);
    } else if (key == 'key') {
        dom.__key = value;
    } else if (isPlainAttr(key, value)) {
        dom.setAttribute(key, value);
    }
}

function Item(props) {
    return <li className="item" style={props.style}>{props.children} <a href="#" onClick={props.onRemoveItem}>X </a></li>;
}
class List extends Component {
    constructor(props) {
    super();
    this.state = {
            list: [
            {
                text: 'aaa',
                color: 'pink'
            },
            {
                text: 'bbb',
                color: 'orange'
            },
            {
                text: 'ccc',
                color: 'yellow'
            }
            ]
        }
}

handleItemRemove(index) {
    this.setState({
        list: this.state.list.filter((item, i) => i !== index)
    });
}
handleAdd() {
    this.setState({
        list: [
            ...this.state.list,
            {
            text: document.getElementById('myRef').value
            }
        ]
    });
}
render() {
    return <div>
        <ul className="list">
            {this.state.list.map((item, index) => {
            return <Item key={`item${index}`} style={{ background: item.color, color: this.state.textColor}} onRemoveItem={() => this.handleItemRemove(index)}>{item.text}</Item>
        })}
        </ul>
        <div>
            <input id='myRef'/>
            <button onClick={this.handleAdd.bind(this)}>add</button>
        </div>
    </div>;
    }
}

const isTextNode = (vdom) => { return typeof vdom === 'string' || typeof vdom === 'number' }
const isElementNode = (vdom) => { return typeof vdom === 'object' && typeof vdom.type === 'string' }
const isEventListenerAttr = (key,value) => { return typeof value === 'function' && key.startsWith('on')}
const isStyleAttr = (key,value) => { return key === 'style' && typeof value === 'object'}
const isPlainAttr = (value) => { return typeof value !== 'object' && typeof value !== 'function'}
const isComponentVdom = (vdom) => { return typeof vdom.type == 'function' }
const isRefAttr = (key, value) => { return key === 'ref' && typeof value === 'function' }

function renderComponent(vdom,parent){
    if (Component.isPrototypeOf(vdom.type)) {
        const instance = new vdom.type(vdom.props);
        const componentVdom = instance.render();
        instance.dom = render(componentVdom, parent);
        instance.dom_instance = instance;
        return instance.dom
    } else {
        //执行type属性上的函数,并将vdom.props属性传入
        const componentVdom = vdom.type(vdom.props);
        //最后执行render函数
        return render(componentVdom, parent);
    }
}

const render = (vdom,parent) => {
    const mount = parent ? (el => parent.appendChild(el)) : (el => el);
    if(isTextNode(vdom)){
        return mount(document.createTextNode(vdom))
    }else if(isElementNode(vdom)){
        const newDom = document.createElement(vdom.type)
        //开始设置节点的属性
        console.log('======vdom',vdom)
        for(let props in vdom.props){
            if(props !== 'children'){//需要将children过滤掉
            console.log('======vdom',props)
            setAttribute(newDom,props,vdom.props[props])
        }
    }
        //对元素的子节点进行递归
        if(vdom.props.children){
            for (const child of vdom.props.children) {
            render(child, newDom);
            }
        }
        return mount(newDom)
    }else if (isComponentVdom(vdom)) {//当我们判断到传入的vdom是由函数组件编译而来的时候
        return renderComponent(vdom,parent)
    }
}
render(<List textColor={'pink'}/>,document.documentElement)