市面上的主流框架,相信作为一个前端搬砖人员,或多或少都会有所接触到。如 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>
Over!