前言
不知道你们在刚开始学习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("*")就会获取如下元素:
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的实现更为复杂科学。