Vue v-for指令为什么能做到循环添加DOM元素?Vue v-for的简单实现。

前言

  不知道你们在刚开始学习Vue、React、Angular这些前端框架时,有没有对其中的指令感到好奇,特别是v-if,v-for,HTML不是标记语言吗?为什么在DOM元素添加上这些指令就可以实现类似编程语言的条件判断和for循环?知道后来我才发现原来实际实现这些功能的还是JS,那么到底怎么实现的呢?

  这里我只学过Vue,所以举例就以Vuev-for来说明。这样同一框架的不同指令、不同框架相同功能的指令都能照葫芦画瓢,知道大概如何实现。当然既然要说明,实现还是用原生JS。还有要说明一点,我的实现方法不是Vue真正的实现方式,我的方法只是可以实现,可能还有其它时间和空间复杂度更好的算法。

如何自己实现一个s-for来模仿v-for的功能?大致可以分以下几个步骤

为了方便说明,我们假设目标元素为

<div s-for="item in list">{
     {
     item}}</div>
1、获取带有s-for属性的DOM元素

  有人可能会好奇这里是不是打错字了,不是指令吗?怎么变成属性了。其实v-for这些指令在HTML标签里实际上就是一个自定义属性,只不过加了这些属性,JS获取后做了一些处理,让他可以执行一些操作,这样称之为指令也无可厚非。
  接下来我们就来说说到底做了哪些处理,执行了什么操作。
  说到用JS获取DOM元素,大家肯定会想到getElementById、getElementsByTagName这些方法,但是仔细一想这些方法貌似都不太试用。因为有s-for属性的标签不一定有id属性,有id属性的标签不一定有s-for属性,同理getElementsByTagName也不行。那么到底要怎样才能拿到包含我们自定义s-for属性的DOM元素呢?
  答案就是document.querySelectorAll()方法。它的功能相比之前两个方法,有更好的普适性。因为他只要传入CSS选择器就可获取到对应的DOM元素,而CSS选择器有一个属性选择器,这样我们就可以获取到页面上所有包含s-for属性的标签。
  这里注意一定要用querySelectorAll()方法,而不能使用querySelector(),因为页面上可能不止有一处地方被我们添加了s-for指令,我们要循环处理querySelectorAll()返回的数组,这样所有s-for指令的元素都能被获取。而querySelector()只能获取匹配到指定选择器的第一个元素。

<div s-for="item in list">{
     {
     item}}</div>
<p s-for="item in list">{
     {
     item}}</p>
<script type="text/javascript">
	var forDirect = document.querySelectorAll('[s-for]');
	for(item of forDirect){
     
		console.log(item);
	}
</script>

console控制台显示我们拿到了我们想要的DOM元素

2、在这里单独讲一下Vue模板字符串是如何实现,接下来的讲解需要用到,本质是字符串替换。

  Vue的模板字符串语法是{ {name}},ES6的语法是`${name}`,实现都差不多。
  主要通过innerHTML拿到对应标签的内容,因为是字符串我们需要用正则匹配到{ {}}里的内容,记为content,因为content应该是一个script里已经定义好的变量,所以我们可以直接eval(content)拿到里面的值。
  模板字符串的实现eval()函数和正则表达式是关键,大家不懂可以先了解一下。eval()函数的主要功能就是将传入的字符串解析成js代码并执行。
  话不多说,上代码。

<div>我的名字叫:{
     {
      name }}</div>
<p>我的年龄:{
     {
      age }}</p>
<!--测试标签嵌套是否也可以替换-->
<div><span>我的年级:{
     {
      classroom }}</span></div>
<script type="text/javascript">
	// 把这里的数据看成是Vue里的data数据
	var name = "lisi";
	var age = 12;
	var classroom = "高一一班";
	function replace(content){
     
		return content.replace(/\{
     {2}([^}]+)\}{2}/g,function(match,string){
     
			try{
     
				return eval(string);
			}catch(e){
     
				return match;
			}
		})
	}
	var body = document.querySelector("body");
	var elements = body.children;
	for(var i = 0; i < elements.length; i++){
     
		elements[i].innerHTML = replace(elements[i].innerHTML);
	}
</script>

实现效果:

Vue v-for指令为什么能做到循环添加DOM元素?Vue v-for的简单实现。_第1张图片

  因为页面上任何一处地方都可能有模板字符串。所以我们需要拿到body下所有标签的一个数组。不能单纯document.querySelectorAll(""),因为 这样html标签也会被选取到,body标签也会选取到,html标签里包含body标签,重复了。同理也不能拿到body元素后,body.querySelectorAll("")。

	var body = document.querySelector("body");
	var elements = body.children;

document.querySelectorAll("*")就会获取如下元素:

Vue v-for指令为什么能做到循环添加DOM元素?Vue v-for的简单实现。_第2张图片
  这里说明一下replace函数(这跟字符串原型上的replace方法不是同一个东西),它的参数接收一个标签的innerHTML。接着看字符串方法replace(),相信大家都用过,第一个参数是正则表达式,大家都知道,关键是第二个参数是一个回调函数,一般大家使用的第二个参数直接传入一个字符串,但是这里我们用回调函数。这个回调函数第一个参数是匹配到的子串,那么他就是{ { name }},第二个参数代表正则第一个括号匹配的结果,那么他就是 name 。这么说大家可能就明白了,当然这个回调函数还有一些其他参数,功能强大,大家可以去MDN官方文档看他说明。另外为什么要try catch捕获异常。这里主要考虑到代码的鲁棒性。有可能开发人员写错了,模板字符串里的并不是一个已经定义好的变量,那么eval()执行的话就会报错导致整个页面无法渲染,我们直接将原字符串作为替换字符串直接返回出去就好了,即不进行替换。
function replace(content){
     
		// 这里的正则是匹配{出现两次、}出现两次,以及他们包围的字符串
		return content.replace(/\{
     {2}([^}]+)\}{2}/g,function(match,string){
     
			try{
     
				return eval(string);
			}catch(e){
     
				return match;
			}
		})
	}

  另外这里如果出于性能优化把script标签放在body里(放在body后面也会),如果代码里有{ { name }},并且确实有name这个属性,那么也会被替换掉。因为script也是body的子标签。解决办法是将script放在head里,并使用window.onload将代码包进去。

3、接下来就比较好解释了,下一步要拿到s-for属性值里的list的值

  这里就比较简单了,字符串匹配list,然后eval()就可以拿到数据了。

<div s-for="item in list">{
     {
     item}}</div>
<script type="text/javascript">
	var list = ["LOL","CF","DNF","QQ"]
	var forDirect = document.querySelectorAll('[s-for]');
	for(item of forDirect){
     
		var value = item.getAttribute('s-for');
		// 因为s-for的属性包含空格,所以我们可以用正则匹配他里面的三个单词
		// 因为变量开头不能是数字,且可能包含下划线和$,所以比较复杂
		var reg = /[a-zA-Z_$]{1}[0-9a-zA-Z_$]*/g;
		// 返回一个包含三个单词的数组,因为list是最后一个元素,所以是[2];
		var aList;
		try {
     
			aList = eval(value.match(reg)[2]);
		} catch(e){
     
			continue;
		}
		for(item of aList){
     
			console.log(item);
		}
	}
</script>

  控制台显示我们拿到了数据。

Vue v-for指令为什么能做到循环添加DOM元素?Vue v-for的简单实现。_第3张图片
4、接下来就是将list数据渲染到页面上了

  这里还有一个难题,s-for属性里的子项item是list的子项,script标签里实际并没有定义这个变量,那么模板字符串就不会替换,因为根本不存在这个变量。

//script里有name这个变量,所以可以替换
<div>我的名字叫:{
     {
      name }}</div>
//script没有item这个变量。所以无法替换
<div s-for="item in items">{
     {
     item}}</div>

  解决这个问题的办法有两种,一个是在script里随便创建一个变量 i 在list 循环中将list子项赋值给他,这样在list循环中 i 就是list的子项,同时把标签里的item全部替换成i,然后就可以用模板字符串替换了(本人没有试验过,觉得比较麻烦,我采用下面第二种方式,这种方式理论也是可以的)。

<div s-for="item in items">{
     {
     item}}</div> => <div s-for="i in items">{
     {
     i}}</div>

  第二种方式在eval()里传入用字符串匹配到的 item ,然后在它的的左侧加上"var “,右侧加上”=list[i]"。在eval()里直接声明这个变量。这样不用更改标签里的内容也能使用模板字符串替换了。(有没有觉得还挺巧妙。。。)

var forDirect = document.querySelectorAll('[s-for]');

var value = item.getAttribute('s-for')
for(var i = 0; i < list.length; i++){
     
	eval("var " + value.match(reg)[0] + " = list[j]")
}
5、说完这些内容,接下来就剩下一些简单的逻辑了。循环之前要拿到该元素的标签名,因为createElement()创建DOM元素时,需要用到标签名。创建好的DOM元素需要添加到父元素里,所以也要拿到该元素的父元素。最后为了提升性能,建议使用文档碎片,新创建的DOM元素先添加到文档碎片里,循环结束后再一并添加到父元素下。最后将该元素从父元素上移除。
<body>
	<ul>
		<li s-for="item in list">
			<span>我叫{
     {
     item.name}}</span>
			<span>{
     {
     item.age}}岁,</span>
			<span>我喜欢{
     {
     item.gf}}</span>
		</li>
	</ul>
	<p>我也不知道写啥</p>
	<div>
		<div s-for="item in list">
			<span>我叫{
     {
     item.name}}</span>
			<span>{
     {
     item.age}}岁,</span>
			<span>我喜欢{
     {
     item.gf}}</span>
		</div>
	</div>
	<script type="text/javascript">
		var list = [{
     name:"张山",age:14,gf:"Cmf"},{
     name:"李四",age:16,gf:"Tsai"},{
     name:"王五",age:18,gf:"CMF"}];
		function replace(content){
     
			return content.replace(/\{
     {2}([^}]+)\}{2}/g,function(match,string){
     
				try{
     
					return eval(string);
				} catch(e) {
     
					return match;
				}
			})
		}
		var forDirect = document.querySelectorAll('[s-for]');
		for(var i = 0; i < forDirect.length; i++){
     
			var value = forDirect[i].getAttribute('s-for');
			var reg = /[a-zA-Z_$]{1}[0-9a-zA-Z_$]*/g;
			var aList = eval(value.match(reg)[2]);
			var parentNode = forDirect[i].parentNode;
			var frag = document.createDocumentFragment();
			for(var j = 0; j < aList.length; j++){
     
				eval("var " + value.match(reg)[0] + " = aList[j]");
				var childNode = document.createElement(forDirect[i].localName);
				childNode.innerHTML = replace(forDirect[i].innerHTML);
				frag.appendChild(childNode);
			}
			parentNode.removeChild(forDirect[i])
			parentNode.appendChild(frag);
		}
	</script>
</body>

  实现效果如下:

Vue v-for指令为什么能做到循环添加DOM元素?Vue v-for的简单实现。_第4张图片

  触类旁通,v-if、v-show的实现也都差不多。区别在于拿到v-if和v-show的DOM元素数组之后,循环的时候做的处理不一样,做的处理不一样功能自然不一样。v-if和v-show都比较好实现,直接拿到指令里面的值放到eval里执行判断真假即可实现对元素的隐藏和显示,v-if是创建删除元素、v-show控制该元素的display属性,处理有一点点小区别。(本人没尝试过,但是实现起来应该没什么大问题)。最后还可以把相关逻辑代码写到js文件里,这样就跟Vue的指令使用没啥区别了。先引脚本文件,然后js里定义好数据,标签里绑定指令就可以使用了。

  当然Vue内部v-for实现考虑肯定更加全面周到,容错率也很高,算法也比较好。我只是最近几天突然来了灵感,解决了我之前学习Vue的一些疑惑,所以把这些分享大家,如果有讲错的地方,请各位大佬及时评论纠正。

你可能感兴趣的:(vue.js,前端,javascript,html5)