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

前言

  不知道你们在刚开始学习Vue、React、Angular这些前端框架时,有没有对其中的指令感到好奇,特别是v-if,v-for,HTML不是标记语言吗?为什么在DOM元素添加上这些指令就可以实现类似编程语言的条件判断和for循环?直到后来我才发现原来这些功能可以通过JS来实现,那么到底怎么实现的呢?这里我说明的实现方式仅代表个人想法,实际框架采用了更好的方式实现,我只是阐明对于v-for用js怎么简单粗暴的实现。

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

为了方便说明,我们假设目标元素为
<div s-for="item in list">{{item}}</div>

1、获取带有s-for属性的DOM元素

  说到用JS获取DOM元素,大家肯定会想到getElementById、getElementsByTagName这些方法,但是仔细一想这些方法貌似都不太试用。因为我们最好通过属性选择器去获取所有加了s-for属性的DOM元素。那么到底要怎样才能拿到包含我们自定义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的模板字符串语法是{{name}},ES6的语法是${name}
  主要通过innerHTML拿到对应标签的内容,因为是字符串我们需要用正则匹配到{{}}里的内容,记为content,因为content应该是一个script里已经定义好的变量,所以我们可以直接eval(content)拿到里面的值。

<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>

实现效果:

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

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

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

  这里说明一下replace函数,它的参数接收一个标签的innerHTML。接着看字符串方法replace(),相信大家都用过,第一个参数是正则表达式,大家都知道,关键是第二个参数是一个回调函数,一般大家使用的第二个参数直接传入一个字符串,但是这里我们用回调函数。这个回调函数第一个参数是匹配到的子串,那么他就是{{ name }},第二个参数代表正则第一个括号匹配的结果,那么他就是 name 。
function replace(content){
		// 这里的正则是匹配{出现两次、}出现两次,以及他们包围的字符串
		return content.replace(/\{{2}([^}]+)\}{2}/g,function(match,string){
			try{
				return eval(string);
			}catch(e){
				return match;
			}
		})
	}

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

3、接下来就比较好解释了,下一步要拿到s-for属性值里的list的值
<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>

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

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>

  实现效果如下:

  哈哈,一个简单粗暴的s-for指令就实现了。。。虽然没什么卵用。实际v-for的实现更为复杂科学。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vgbire

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值