从JS继承实现一个VueJs的render函数

javascript/jquery

浏览数:372

2019-10-2

市面上的主流框架,相信作为一个前端搬砖人员,或多或少都会有所接触到。如ReactJs、VueJs、AngularJs。那么对于每个框架的使用来说其实是比较简单的,还记得上大学时候,老师曾经说过:”技术就是窗户纸,捅一捅就破了”,也就是说,任何一门技术,只要深入去研究,那么它也不再是很神秘的东西了。我个人在工作中用VueJs是比较多的,当然React也会,那今天就为大家来实现一个Vuejs框架中的render函数

首先来看一段代码:

    <div id="div1">
        <span>test</span>
        <Tab></Tab>
        <UserLogin></UserLogin>
    </div>

最终在页面上的呈现是怎样的呢?

毫无疑问,只看到了test这一段文本内容。因为html不认识Tab、UserLogin这两个”异类”元素。那么假如现在要实现的是,通过一个render方法:

        render({
            root:'#div1',
            components:{
                Tab,UserLogin
            }
        })

将Tab、UserLogin这两个组件的内容渲染出来,该去怎样实现呢?这里涉及到的知识点如下:

  • 类型判断
  • DOM操作
  • Js的继承、多态
  • 组件化思想

首先通过Js的继承及组件化思想来定义两个类Tab、UserLogin,它们都有一个自身的render方法(从父类Component)继承而来并进行了重写。直接上代码:

Component类:

 class Component{
        render(){
            throw new Error('render is required');
        }

    }

Tab类:

    class Tab extends Component{
        render(){
            let div1 = document.createElement('div');
            div1.innerHTML = '我是Tab组件';
            return div1;
        }
    }

UserLogin类:

    class UserLogin extends Component{
        render(){
            let div2 = document.createElement('div');
            div2.innerHTML = "我是登录组件"
            return div2
        }
    }

到这里,相信大家学过ES6的,对这样的代码都是感觉很熟悉的,重点是render函数究竟该怎样去实现。再来看一下render函数的基本骨架:

    render({
        root:'#div1',
        components:{
            Tab,UserLogin
        }
    })

render函数接收了一个参数(对象类型),里面包括两个属性root(挂载的元素),以及components(带渲染的组件类)。对于render的实现,先从root这个属性入手。灵魂拷问,root属性一定是某个元素的id吗?对于一个函数的参数来说,使用者传递什么类型都是可以的,但只要符合规定的参数才能有效,说那么多其实就是需要对render函数对象参数的root属性进行校验。代码如下:

    function render(opts){
        let root = null;
        if(typeof opts.root === "string"){
            root = document.querySelector(opts.root);
            if(!root){
                throw new Error(`can't found ${opts.root}`)
            }
        }else if(opts.root instanceof HTMLElement){
            root = opts.root
        }else{
            throw new Error(`root invalid`)
        }
    }
    

这里的操作的目的就是为了找到root这个(父)元素。

接下来就是针对参数对象的components属性来进行处理了,也就是说需要找到所有自定义元素(Tab、UserLogin),又一次灵魂拷问,可以通过父元素找到其包含的所有子元素,但是该怎样去区分哪些元素是自定义的呢?先来看一下通过父元素找到所有子元素的代码:

let elements = root.getElementsByTagName("*");

打印看看elements是怎样的数据结构:

可以看到,有一个是我们很熟悉的自有元素span,还有两个未知的元素tab、userlogin,这时你可能回想,将elements转换为数组、然后遍历进行判断是否有自定义元素,哎,其实这思路还不错。看下代码:

  Array.from(elements).forEach((ele) =>{
       if(ele.tagName ==='tab'){
         //找到了自定义元素tab
       }
       if(ele.tagName ==='userlogin'){
         //找到了自定义元素userlogin  
       }     
  })

这样行吗?显然是不行的。万一将元素标签结构改成这样呢

<div id="div1">
    <span>test</span>
    <Tab></Tab>
    <UserLogin></UserLogin>
    <List></List>
</div>

那是不是要写很多个if判断,显示不对。我们知道,一个html文档的继承结构大致如下:

对于上述的自有元素span,它继承是HTMLElement,也就是说span元素的构造函数是HTMLSpanElement,那么对于上述两个未知元素,它们的构造函数是什么呢?其实是HTMLUnknownElement。这下就可以通过构造函数类再结合参数对象中components属性来找到未知元素了,代码如下:

        Array.from(elements).forEach((ele) =>{
            if(ele.constructor === HTMLUnknownElement){
                for(let compName in opts.components){
                    if(compName.toLowerCase() === ele.tagName.toLowerCase()){
                        let CmpCls = opts.components[compName];
                    }
                }
            }
        })

代码中CmpCls其实就是我们最初定义的两个类Tab、UserLogin,然后通过实例化它们,并调用各自实例对象的render方法,再通过找到的未知元素ele来进行父元素(root)里内容的替换渲染了。代码如下:

        Array.from(elements).forEach((ele) =>{
            if(ele.constructor === HTMLUnknownElement){
                for(let compName in opts.components){
                    if(compName.toLowerCase() === ele.tagName.toLowerCase()){
                        let CmpCls = opts.components[compName];
                        let cmp = new CmpCls();
                        let res = cmp.render();
                        ele.parentNode.replaceChild(res,ele);
                    }
                }
            }
        })

再看下页面最终呈现的内容:

正确地将我们自定义Tab、UserLogin两个组件的内容渲染了出来。

最终完整代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>实现render函数</title>
</head>
<body>
    <div id="div1">
        <span>test</span>
        <Tab></Tab>
        <UserLogin></UserLogin>
    </div>
    <script>

        function render(opts){
            //1.找到root
            let root = null;
            if(typeof opts.root === "string"){
                root = document.querySelector(opts.root);
                if(!root){
                    throw new Error(`can't found ${opts.root}`)
                }
            }else if(opts.root instanceof HTMLElement){
                root = opts.root
            }else{
                throw new Error(`root invalid`)
            }

            //2.找出所有自定义的元素
            let elements = root.getElementsByTagName("*");
            Array.from(elements).forEach((ele) =>{
                if(ele.constructor === HTMLUnknownElement){
                    for(let compName in opts.components){
                        if(compName.toLowerCase() === ele.tagName.toLowerCase()){
                            let CmpCls = opts.components[compName];
                            let cmp = new CmpCls();
                            let res = cmp.render();
                            ele.parentNode.replaceChild(res,ele);
                        }
                    }
                }
            })

        }

        class Component{
            render(){
                throw new Error('render is required');
            }

        }

        class Tab extends Component{
            render(){
                let div1 = document.createElement('div');
                div1.innerHTML = '我是Tab组件';
                return div1;
            }
        }

        class UserLogin extends Component{
            render(){
                let div2 = document.createElement('div');
                div2.innerHTML = "我是登录组件"
                return div2
            }
        }
        render({
            root:'#div1',
            components:{
                Tab,UserLogin
            }
        })
    </script>
    
</body>
</html>

就这样,一个简洁版的Vuejs render函数就实现了, 与Vuejs中的render函数相比,还差很多很多技术点未实现。但并不阻碍我们来了解部分实现思想。

作者:darkCode