简单实现虚拟dom

虚拟dom是将dom结构表示成一个对象,当组件的状态发生更改时,新的虚拟dom会与dom进行比较,只将修改的部分替换掉,而不需要重新渲染整个dom。
下面我们将分为四步逐步实现我们的虚拟dom:
1、创建hyperscriptfunction来实现dom渲染
2、创建一个简单的应用程序,使用hyperscript来渲染
3、使用虚拟dom实现动态渲染
4、实现diffing算法

hyperscript

在这一步中,先看一下如何用它构建简单的 DOM,在进一步的步骤中,我们将开发它来制作虚拟 DOM。

function hyperscript(nodeName, rootNode, attr, ...children) {
    const $el = document.createElement(nodeName);
    for (let key in attr) {

        $el.setAttribute(key, attr[key]);
    }
    // $el.innerHTML=children.join()
    children.forEach(child => {
        if (typeof child === 'string') {
            $el.appendChild(document.createTextNode(child));
        } else {
            $el.appendChild(child);
        }
    });
    rootNode && rootNode.appendChild($el)
    return $el;
}

hyperscript首先是创建了一个简单的dom节点并带有nodeName、attributes、children,渲染children可以使用递归的方式,这里只是简单实现看效果,难点通通扔到后面。下面来调用这个hyperscript

hyperscript(
    'div',
    document.body,
    null,
    'Hello, vDOM'
);

结果:
在这里插入图片描述

创建一个简单的应用


const App = (props) => {
    return hyperscript('div', document.body, { class: 'app'},
    hyperscript('h1', null, null, 'Simple vDOM'),
    hyperscript(
        'ul', null, null,
        ...props.map(item => hyperscript('li', null, null, item))
    ));
}

let currentApp;
const render= (state) => {
    const newApp = App(state);
    currentApp ? document.body.replaceChild(newApp, currentApp) : document.body.appendChild(newApp);
    currentApp = newApp;
};

let state = [ 
        '🌑', '🌒', '🌓','🌔', '🌕', '🌖','🌗', '🌘', '🌙'
];

setInterval(() => {
    state = [...state, state[Math.floor(Math.random()*state.length)]];
    render(state);
},1000);

App创建了一个div节点,并带有一个h1标题和一个ul列表,每隔一秒钟向props中添加一个表情,并重新执行App,效果懒得录屏了,贴张图算了。简单描述一下就是没过一秒整个div都会重新被渲染

在这里插入图片描述

实现 vDOM 渲染

之前已经提到过了,所谓的vDOM本质上是一个结构类似dom的object,所以我们先重构一下hyperscript,非常简单的重构完成了。

function hyperscript(nodeName, attr, ...children) {
    return {nodeName, attr, children}
 }
 

可以再console里面打印一下结果,垃圾网络毁我青春不传了

下面实现以下渲染函数,将object转为dom

 const renderNode = vnode => {
     let $el;
     const {nodeName, attr, children} = vnode;
     if (vnode.split) return document.createTextNode(vnode);
     $el = document.createElement(nodeName);
     for (let key in attr) {
 
         $el.setAttribute(key, attr[key]);
     }
     children.forEach(child => {
         if (typeof child === 'string') {
             $el.appendChild(document.createTextNode(child));
         } else {
             $el.appendChild(child);
         }
     });
     return $el;
 } 

让我一步一步解释它的作用:

1、使用解构我们检索虚拟节点的节点名称、属性和子节点
2、如果 vnode 是文本(我们可以通过 vnode.split 检查)然后我们返回文本节点
3、否则,我们使用 nodeName 创建一个元素并从属性对象设置其属性
4、如果有的话,为child节点做同样的事情
下面把整体的代码贴一下

function hyperscript(nodeName, attr, ...children) {
    return {nodeName, attr, children}
 }

 
 const App = (props) => {
     return renderNode(hyperscript('div', { class: 'app'},
     renderNode(hyperscript('h1', null, 'Simple vDOM')),
     renderNode(hyperscript(
         'ul', null, 
         ...props.map(item => renderNode(hyperscript('li', null, item)))
     ))));
 }
 
 let state = [ 
         '🌑', '🌒', '🌓','🌔', '🌕', '🌖','🌗', '🌘', '🌙'
 ];
 
 const renderNode = vnode => {
     let $el;
     const {nodeName, attr, children} = vnode;
     if (vnode.split) return document.createTextNode(vnode);
     $el = document.createElement(nodeName);
     for (let key in attr) {
 
         $el.setAttribute(key, attr[key]);
     }
     children.forEach(child => {
         if (typeof child === 'string') {
             $el.appendChild(document.createTextNode(child));
         } else {
             $el.appendChild(child);
         }
     });
     return $el;
 } 
 let currentApp;
 const render= (state) => {
     const newApp = App(state);
     currentApp ? document.body.replaceChild(newApp, currentApp) : document.body.appendChild(newApp);
     currentApp = newApp;
 };
 
 setInterval(() => {
     state = [...state, state[Math.floor(Math.random()*state.length)]];
     render(state);
 },1000);
 

到这里我们就实现了将虚拟dom(一个类似dom的object)转化为dom并渲染的功能。下面剩下的也就是细化和diff算法了。
前面几步结束后,我们会发现我们都是直接替换了整个div,H1标题并没有发生变化,但还是被我们重新渲染了,接下来我们将ul标签拆出来,创建一个单独的people component 来单独存放 emoji list,这里引入了render方法 —— 它实际上创建了我们用来渲染真实DOM的虚拟DOM。

class People extends Component{
    constructor(props) {
        super(props);
        this.state = {
             list: [
                '🌑', '🌒', '🌓','🌔', '🌕', '🌖','🌗', '🌘', '🌙'
            ]
        };
    }
    render(props, state) {
        return hyperscript(
          'ul', null,
          ...state.list.map(item => hyperscript('li', null, item))
        )
      }
}

现在需要一个函数用来设置emoji list的值,为其列表添加值,并且这也是每一个组件都需要的,所以直接写一个父类,让其他类都集成于他。

 class Component {
    constructor(props) {
        this.props = props
        this.state = {}
    }
    setState(state) {
        this.state = Object.assign({}, state);
        renderComponent(this)
    }
}

基本上,我们只是使用 props 和 state 字段启动我们的组件并实现 setState 方法,该方法基本上重写我们的state并调用其自身的 renderComponent(重新渲染) ,将app、people都加上对component的继承,接下来实现这个renderComponent,他的用处就是重新渲染传入的组件。需要做三件事
1、获取更改前的dom(存储在component.base)
2、调用组件的render方法渲染出虚拟dom
3、获取父节点 并使用新的去替换掉旧的孩子节点

const renderComponent = (component, parent) => {
    const oldBase = component.base;
    component.base = renderNode(
        component.render(component.props, component.state)
    )
    if (parent) {
        parent.appendChild(component.base);
    } else {
        oldBase.parentNode.replaceChild(component.base, oldBase);
    }
}

接下来需要rendernode做一定的适配性的改动,作用还是原来的作用,现在nodeName 不仅可以采用字符串值,还可以采用另一个组件/函数进行渲染,在我的示例中,这是 People 组件, 我们需要确保我们正确处理它们 (正确处理:能够渲染该组件何其子组件)。 这是我们更新的 renderNode 实现:

const renderNode = vnode => {
    let $el;
    const {nodeName, attr, children} = vnode;
    if (vnode.split) return document.createTextNode(vnode);
    if (typeof nodeName === 'string') {
        $el = document.createElement(nodeName);
        for (let key in attr) {
    
            $el.setAttribute(key, attr[key]);
        }
    } else if (typeof nodeName === 'function') {
        const component = new nodeName(attr);
        $el = renderNode(component.render(component.props, component.state));
        component.base = $el;
    }
   
    (children || []).forEach(child => $el.appendChild(renderNode(child)))
    return $el;
} 

现在App就不再像之前那么复杂了,就只处理dom元素就好,

class App extends Component {
    render() {
      return hyperscript('div', { class: 'app' },
        hyperscript('h1', null, 'Simple vDOM'),
        hyperscript(People)
      )
    }
  };

将之前的setInterval插入到people中,现在的效果就是,每次添加一个emoji就只有ul变了二h1并不会被重新渲染,传个图片太难了我放弃了。

差分算法

在我们本次项目中,只需要实现一下几个功能
1、如果是第一次被渲染,也就是说没有真实的dom,那么就直接插入到父节点下面
2、如果存在dom,使用 component.render 方法创建虚拟 DOM,然后将其与当前 DOM 状态(我们保存在 component.base 字段中)进行比较,并将 diff 的结果再次保存在其中。diff 函数应该检查是否有任何新的孩子,如果是,我们就append它。

const diff = (dom, vnode, parent) => {
    if (dom) {
        if (typeof vnode === 'string') {
          dom.nodeValue = vnode
          return dom
        } 
        if (typeof vnode.nodeName === 'function') {
          const component = new vnode.nodeName(vnode.attributes)
          const rendered = component.render(component.props, component.state)
    
          diff(dom, rendered)
          return dom
        }
        if (vnode.children.length !== dom.childNodes.length) {
            console.log(dom.childNodes.length);
            console.log(dom.childNodes);
          dom.appendChild(
            renderNode(vnode.children[vnode.children.length - 1])
          )
        }

        dom.childNodes.forEach((child, i) => diff(child, vnode.children[i]))
    
        return dom
      } else {
        const newDom = renderNode(vnode)
        parent.appendChild(newDom)
        return newDom
      }
}

更新一张效果图片
在这里插入图片描述

总结

真实的react还需要对比类型、属性、children。。这是一个简单到不能再简单的例子,与真实的react还相差甚远,实现一个虚拟dom不是说直接把虚拟dom完整的写出来,而是为了更好的理解其中的概念和思想。下面是最后的一个完整的代码:

function hyperscript(nodeName, attr, ...children) {
    return {nodeName, attr, children}
 }

 class Component {
    constructor(props) {
        this.props = props
        this.state = {}
    }
    setState(state) {
        this.state = Object.assign({}, state);
        renderComponent(this)
    }
}
class People extends Component{
    constructor(props) {
        super(props);
        this.state = {
             list: [
                '🌑', '🌒', '🌓','🌔', '🌕', '🌖','🌗', '🌘', '🌙'
            ]
        };
        this.timer = setInterval(_ => {
            this.setState({
                list: [...this.state.list, this.state.list[Math.floor(Math.random()*this.state.list.length)]]
            });
        }, 1000);
    }
    render(props, state) {
        return hyperscript(
          'ul', null,
          ...state.list.map(item => hyperscript('li', null, item))
        )
      }
}


class App extends Component {
    render() {
      return hyperscript('div', { class: 'app' },
        hyperscript('h1', null, 'Simple vDOM'),
        hyperscript(People)
      )
    }
  };


const renderComponent = (component) => {
   let rendered = component.render(component.props, component.state);
   component.base = diff(component.base, rendered)
}



const renderNode = vnode => {
    let $el;
    const {nodeName, attr, children} = vnode;
    if (vnode.split) return document.createTextNode(vnode);
    if (typeof nodeName === 'string') {
        $el = document.createElement(nodeName);
        for (let key in attr) {
    
            $el.setAttribute(key, attr[key]);
        }
    } else if (typeof nodeName === 'function') {
        const component = new nodeName(attr);
        $el = renderNode(component.render(component.props, component.state));
        component.base = $el;
    }
   
    (children || []).forEach(child => $el.appendChild(renderNode(child)))
    return $el;
} 


const diff = (dom, vnode, parent) => {
    if (dom) {
        if (typeof vnode === 'string') {
          dom.nodeValue = vnode
          return dom
        } 
        if (typeof vnode.nodeName === 'function') {
          const component = new vnode.nodeName(vnode.attributes)
          const rendered = component.render(component.props, component.state)
    
          diff(dom, rendered)
          return dom
        }
        if (vnode.children.length !== dom.childNodes.length) {
            console.log(dom.childNodes.length);
            console.log(dom.childNodes);
          dom.appendChild(
            renderNode(vnode.children[vnode.children.length - 1])
          )
        }

        dom.childNodes.forEach((child, i) => diff(child, vnode.children[i]))
    
        return dom
      } else {
        const newDom = renderNode(vnode)
        parent.appendChild(newDom)
        return newDom
      }
}

const render = (vnode, parent) => {
    diff(undefined, vnode, parent)
}

render(hyperscript(App), document.body);

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值