个视频用CSS来写聚光灯的效果,大家可以先想一下思路。
·来看一下代码,这里就写了一个h1标签,还有一些基本的样式。
·先来看下布局,这里需要把hello world多准备一份,可以直接复制一份h1或者添加一个伪元素,保持html结构简洁一点。
·<htmicontent里面可以直接把hello world写在里面,或者用之前说的attr()属性获取函数,然后定义一个data-text属性,在h1里面就来定义这个属性,再把hello world放在里面,这样子也可以,再给它定一下位。
·现在这两个hello world通过定位重叠在一起了,然后要对伪元素的hello world裁切一小部分出来。
·这里用clip-path属性裁一个小圆出来用circle,半径大小为100像素,位置在水平的最左边。现在只是显示了水平最左边的这小部分了,把它调到中间位置看一下。
·是不是可以通过一个动画控制它水平方向的值就可以了?来写个动画,这里设置三个区间,一开始的时候是在最左边的,100%的时候也是回到最左边,50%的时候就去到最右边,这里改成100%就可以了。
·来绑定一下动画,聚光灯的效果其实就已经完成了,但是背景不怎么好看,给它加一个渐变的背景。
·这里准备了一张比较好看的渐变背景图,再给它放大一点,背景图是出来了,但是要让背景只是显示在文字里面,这里就要对背景进行裁切,让它按照文本来裁切。
·现在背景其实已经裁出来了,只是因为字体颜色把背景给挡住了,把字体的颜色改成透明就可以了,动画的时间稍微改长一点。
聚光灯的效果就完成了。
这个视频就到这里,感谢大家的收看。
知道浏览器和服务端是通过 HTTP 协议进行数据传输的,而 HTTP 协议又是纯文本协议,那么浏览器在得到服务端传输过来的 HTML 字符串,是如何解析成真实的 DOM 元素的呢,也就是我们常说的生成 DOM Tree,最近了解到状态机这样一个概念,于是就萌生一个想法,实现一个 innerHTML 功能的函数,也算是小小的实践一下。
函数原型
我们实现一个如下的函数,参数是 DOM 元素和 HTML 字符串,将 HTML 字符串转换成真实的 DOM 元素且 append 在参数一传入的 DOM 元素中。
function html(element, htmlString) { // 1. 词法分析 // 2. 语法分析 // 3. 解释执行 } 复制代码
在上面的注释我已经注明,这个步骤我们分成三个部分,分别是词法分析、语法分析和解释执行。
词法分析
词法分析是特别重要且核心的一部分,具体任务就是:把字符流变成 token 流。
词法分析通常有两种方案,一种是状态机,一种是正则表达式,它们是等效的,选择你喜欢的就好。我们这里选择状态机。
首先我们需要确定 token 种类,我们这里不考虑太复杂的情况,因为我们只对原理进行学习,不可能像浏览器那样有强大的容错能力。除了不考虑容错之外,对于自闭合节点、注释、CDATA 节点暂时均不做考虑。
接下来步入主题,假设我们有如下节点信息,我们会分出哪些 token 来呢。
<p class="a" data="js">测试元素</p> 复制代码
对于上述节点信息,我们可以拆分出如下 token
状态机的原理,将整个 HTML 字符串进行遍历,每次读取一个字符,都要进行一次决策(下一个字符处于哪个状态),而且这个决策是和当前状态有关的,这样一来,读取的过程就会得到一个又一个完整的 token,记录到我们最终需要的 tokens 中。
万事开头难,我们首先要确定起初可能处于哪种状态,也就是确定一个 start 函数,在这之前,对词法分析类进行简单的封装,具体如下
function HTMLLexicalParser(htmlString, tokenHandler) { this.token=[]; this.tokens=[]; this.htmlString=htmlString this.tokenHandler=tokenHandler } 复制代码
简单解释下上面的每个属性
我们可以很容易的知道,字符串要么以普通文本开头,要么以<开头,因此 start 代码如下
HTMLLexicalParser.prototype.start=function(c) { if(c==='<') { this.token.push(c) return this.tagState } else { return this.textState(c) } } 复制代码
start处理的比较简单,如果是<字符,表示开始标签或结束标签,因此我们需要下一个字符信息才能确定到底是哪一类 token,所以返回tagState函数去进行再判断,否则我们就认为是文本节点,返回文本状态函数。
接下来分别展开tagState和textState函数。tagState根据下一个字符,判断进入开始标签状态还是结束标签状态,如果是/表示是结束标签,否则是开始标签,textState用来处理每一个文本节点字符,遇到<表示得到一个完整的文本节点 token,代码如下
HTMLLexicalParser.prototype.tagState=function(c) { this.token.push(c) if(c==='/') { return this.endTagState } else { return this.startTagState } } HTMLLexicalParser.prototype.textState=function(c) { if(c==='<') { this.emitToken('text', this.token.join('')) this.token=[] return this.start(c) } else { this.token.push(c) return this.textState } } 复制代码
这里初次见面的函数是emitToken、startTagState和endTagState。
emitToken用来将产生的完整 token 存储在 tokens 中,参数是 token 类型和值。
startTagState用来处理开始标签,这里有三种情形
endTagState用来处理结束标签,结束标签不存在属性,因此只有两种情形
逻辑上面说的比较清楚了,代码也比较简单,看看就好啦
HTMLLexicalParser.prototype.emitToken=function(type, value) { var res={ type, value } this.tokens.push(res) // 流式处理 this.tokenHandler && this.tokenHandler(res) } HTMLLexicalParser.prototype.startTagState=function(c) { if(c.match(/[a-zA-Z]/)) { this.token.push(c.toLowerCase()) return this.startTagState } if(c===' ') { this.emitToken('startTag', this.token.join('')) this.token=[] return this.attrState } if(c==='>') { this.emitToken('startTag', this.token.join('')) this.token=[] return this.start } } HTMLLexicalParser.prototype.endTagState=function(c) { if(c.match(/[a-zA-Z]/)) { this.token.push(c.toLowerCase()) return this.endTagState } if(c==='>') { this.token.push(c) this.emitToken('endTag', this.token.join('')) this.token=[] return this.start } } 复制代码
最后只有属性标签需要处理了,也就是上面看到的attrState函数,也处理三种情形
代码如下
HTMLLexicalParser.prototype.attrState=function(c) { if(c.match(/[a-zA-Z'"=]/)) { this.token.push(c) return this.attrState } if(c===' ') { this.emitToken('attr', this.token.join('')) this.token=[] return this.attrState } if(c==='>') { this.emitToken('attr', this.token.join('')) this.token=[] return this.start } } 复制代码
最后我们提供一个parse解析函数,和可能用到的getOutPut函数来获取结果即可,就不啰嗦了,上代码
HTMLLexicalParser.prototype.parse=function() { var state=this.start; for(var c of this.htmlString.split('')) { state=state.bind(this)(c) } } HTMLLexicalParser.prototype.getOutPut=function() { return this.tokens } 复制代码
接下来简单测试一下,对于<p class="a" data="js">测试并列元素的</p><p class="a" data="js">测试并列元素的</p>HTML 字符串,输出结果为
看上去结果很 nice,接下来进入语法分析步骤
语法分析
首先们需要考虑到的情况有两种,一种是有多个根元素的,一种是只有一个根元素的。
我们的节点有两种类型,文本节点和正常节点,因此声明两个数据结构。
function Element(tagName) { this.tagName=tagName this.attr={} this.childNodes=[] } function Text(value) { this.value=value || '' } 复制代码
目标:将元素建立起父子关系,因为真实的 DOM 结构就是父子关系,这里我一开始实践的时候,将 childNodes 属性的处理放在了 startTag token 中,还给 Element 增加了 isEnd 属性,实属愚蠢,不但复杂化了,而且还很难实现。仔细思考 DOM 结构,token 也是有顺序的,合理利用栈数据结构,这个问题就变的简单了,将 childNodes 处理放在 endTag 中处理。具体逻辑如下
代码如下
function HTMLSyntacticalParser() { this.stack=[] this.stacks=[] } HTMLSyntacticalParser.prototype.getOutPut=function() { return this.stacks } // 一开始搞复杂了,合理利用基本数据结构真是一件很酷炫的事 HTMLSyntacticalParser.prototype.receiveInput=function(token) { var stack=this.stack if(token.type==='startTag') { stack.push(new Element(token.value.substring(1))) } else if(token.type==='attr') { var t=token.value.split('='), key=t[0], value=t[1].replace(/'|"/g, '') stack[stack.length - 1].attr[key]=value } else if(token.type==='text') { if(stack.length) { stack[stack.length - 1].childNodes.push(new Text(token.value)) } else { this.stacks.push(new Text(token.value)) } } else if(token.type==='endTag') { var parsedTag=stack.pop() if(stack.length) { stack[stack.length - 1].childNodes.push(parsedTag) } else { this.stacks.push(parsedTag) } } } 复制代码
简单测试如下:
没啥大问题哈
解释执行
对于上述语法分析的结果,可以理解成 vdom 结构了,接下来就是映射成真实的 DOM,这里其实比较简单,用下递归即可,直接上代码吧
function vdomToDom(array) { var res=[] for(let item of array) { res.push(handleDom(item)) } return res } function handleDom(item) { if(item instanceof Element) { var element=document.createElement(item.tagName) for(let key in item.attr) { element.setAttribute(key, item.attr[key]) } if(item.childNodes.length) { for(let i=0; i < item.childNodes.length; i++) { element.appendChild(handleDom(item.childNodes[i])) } } return element } else if(item instanceof Text) { return document.createTextNode(item.value) } } 复制代码
实现函数
上面三步骤完成后,来到了最后一步,实现最开始提出的函数
function html(element, htmlString) { // parseHTML var syntacticalParser=new HTMLSyntacticalParser() var lexicalParser=new HTMLLexicalParser(htmlString, syntacticalParser.receiveInput.bind(syntacticalParser)) lexicalParser.parse() var dom=vdomToDom(syntacticalParser.getOutPut()) var fragment=document.createDocumentFragment() dom.forEach(item=> { fragment.appendChild(item) }) element.appendChild(fragment) } 复制代码
三个不同情况的测试用例简单测试下
html(document.getElementById('app'), '<p class="a" data="js">测试并列元素的</p><p class="a" data="js">测试并列元素的</p>') html(document.getElementById('app'), '测试<div>你好呀,我测试一下没有深层元素的</div>') html(document.getElementById('app'), '<div class="div"><p class="p">测试一下嵌套很深的<span class="span">p的子元素</span></p><span>p同级别</span></div>') 复制代码
声明:简单测试下都没啥问题,本次实践的目的是对 DOM 这一块通过词法分析和语法分析生成 DOM Tree 有一个基本的认识,所以细节问题肯定还是存在很多的。
总结
其实在了解了原理之后,这一块代码写下来,并没有太大的难度,但却让我很兴奋,有两个成果吧
*1.深拷贝*/
//(1)
function deepClone(obj) {
if (!obj && typeof obj !=='object') {
return;
}
var newObj=obj.constructor===Array ? [] : {};
for (var key in obj) {
if (obj[key]) {
if (obj[key] && typeof obj[key]==='object') {
newObj[key]=obj[key].constructor===Array ? [] : {};
//递归
newObj[key]=deepClone(obj[key]);
} else {
newObj[key]=obj[key];
}
}
}
return newObj;
}
//使用方法:
var _arr=[{ a: 1, b: 2 }, { c: 3, d: 4 }];
var _newArr=deepClone(_arr);
//(2)比较粗鲁的复制
JSON.parse(JSON.stringify(_arr));
/*2.合并数组*/
//(1)concat方法
var _arr1=["a", "b", "c"];
var _arr2=["1", "2", "3"];
var newArr=_arr1.concat(_arr2);
// 注:使用concat方法合并数组时,不会改变原有数组的结构。
//(2)push.apply()方法
var _arr1=["a", "b", "c"];
var _arr2=["1", "2", "3"];
_arr1.push.apply(_arr1, _arr2);
//使用此方法合并的数组会改变合并到的数组
/*3.判断数据类型(准确)*/
Object.prototype.toString.call();
//使用方法:
var _obj={};
Object.prototype.toString.call(_obj)==[object Object];
/*4.将函数中的arguments参数转成数组*/
var _arguments=Array.prototype.slice.apply(arguments);
//使用方法:
function add(x, y) {
var _argus=Array.prototype.slice.call(arguments);
console.log(_argus);
}
/*5.节流函数*/
function delayFn(fn, delay, mustDelay) {
var timer=null;
var t_start;
return function() {
var context=this,
args=arguments,
t_cur=+new Date();
clearTimeout(timer); //清除上一次定时器
if (!t_start) {
t_start=t_cur; //设置触发事件
}
if (t_cur - t_start >=mustDelay) { //判断时间差是否大于间隔时间
fn.apply(context, args);
t_start=t_cur;
} else {
timer=setTimeout(function() { //延迟执行函数
fn.apply(context, args);
}, delay);
}
};
}
//使用方法:
var count=0;
function fn() {
count++;
console.log(count);
}
document.onmousemove=delayFn(fn, 100, 200); //100ms内连续触发的调用,后一个调用会把前一个调用的等待处理掉,但每隔200ms至少执行一次
/*6累加函数*/
var arr1=arr.reduce(function(con, info) {
if (con) {
return con;
}
return info._show;
}, false)
/*7手机端收起弹出键盘*/
var hideKeyboard=function() {
document.activeElement.blur(); //ios
$("input").blur();
};
/*8时间格式化*/
Date.prototype.Format=function(fmt) {
var o={
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) fmt=fmt.replace(RegExp., (this.getFullYear() + "").substr(4 - RegExp..length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt=fmt.replace(RegExp., (RegExp..length==1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
//使用方法:(时间转成yyyy-MM-dd hh:mm:ss)
var time1=new Date().Format("yyyy-MM-dd");
var time2=new Date().Format("yyyy-MM-dd hh:mm:ss");
/*9标准时间转时间戳*/
var time1=Date.parse(new Date());
/*10时间戳转标准时间*/
new Date(time1).Format("yyyy-MM-dd hh:mm:ss");
/*11.JSON数组去重*/
/*
* JSON数组去重
* @param: [array] json Array
* @param: [string] 唯一的key名,根据此键名进行去重
*/
function uniqueArray(array, key) {
var result=[array[0]];
for (var i=1; i < array.length; i++) {
var item=array[i];
var repeat=false;
for (var j=0; j < result.length; j++) {
if (item[key]==result[j][key]) {
repeat=true;
break;
}
}
if (!repeat) {
result.push(item);
}
}
return result;
}
//使用方法:
var _json=[{ id: 'aaa', name: 'bb' }, { id: 'bbb', name: 'xx' }, { id: 'aaa', name: 'ccc' }]
uniqueArray(_json, 'id');
/*12.JSON数组去重多字段*/
var arr=[
{ "name": "1", "value": "qqq", "age": "10" },
{ "name": "1", "value": "qqq", "age": "10" },
{ "name": "2", "value": "eee", "age": "20" },
{ "name": "4", "value": "rrr", "age": "50" },
{ "name": "5", "value": "ttt", "age": "100" }
];
for (var i=0; i < arr.length; i++) {
for (var j=i + 1; j < arr.length;) {
if (arr[i].name==arr[j].name && arr[i].value==arr[j].value && arr[i].age==arr[j].age) {
arr.splice(j, 1);
} else j++;
}
}
//*13.普通数组去重*/
Array.prototype.unique=function() {
var res=[];
var json={};
for (var i=0; i < this.length; i++) {
if (!json[this[i]]) {
res.push(this[i]);
json[this[i]]=1;
}
}
return res;
}
//使用方法:
var arr=[112,112,34,'xx',112,112,34,'xx','str','str1'];
unique(arr);
/*14.console.log && console.dir*/
//使用方法:
var obj={name:'xx',age:18};
console.log(obj);
console.dir(obj);
//区别
//console.dir比log方法更易读,信息也更丰富。能够输出原始属性
/*14.获取当前时间的时间戳(三种方法)*/
var d1=new Date().getTime();
var d2=new Date().valueOf();
var d3=+new Date();
/*15 替代arguments.callee()方法*/
//给内部函数命名
//接受参数n=5,不用for循环输出数组【1,2,3,4,5】
//(1)arguments.callee()递归写法
function show(n) {
var arr=[];
return (function () {
arr.unshift(n);
n--;
if (n !=0) {
arguments.callee();
}
return arr;
})()
}
show(5)//[1,2,3,4,5]
//(2)内部函数明明写法
function show(n) {
var arr=[];
return (function fn() {
arr.unshift(n);
n--;
if (n !=0) {
fn();
}
return arr;
})()
}
show(5)//[1,2,3,4,5]
/*16 预防XSS攻击*/
//(1)使用正则替换关键字符
function safeStr(str){
return str.replace(/</g,'<').replace(/>/g,'>').replace(/"/g, """).replace(/'/g, "'");
}
//(2)使用正则转码,解码
var HtmlUtil={
/*1.用正则表达式实现html转码*/
htmlEncodeByRegExp:function (str){
var s="";
if(str.length==0) return "";
s=str.replace(/&/g,"&");
s=s.replace(/</g,"<");
s=s.replace(/>/g,">");
s=s.replace(/ /g," ");
s=s.replace(/\'/g,"'");
s=s.replace(/\"/g,""");
return s;
},
/*2.用正则表达式实现html解码*/
htmlDecodeByRegExp:function (str){
var s="";
if(str.length==0) return "";
s=str.replace(/&/g,"&");
s=s.replace(/</g,"<");
s=s.replace(/>/g,">");
s=s.replace(/ /g," ");
s=s.replace(/'/g,"\'");
s=s.replace(/"/g,"\"");
return s;
}
};
/*17。多维数组转一维数组*/ /*参考https://www.cnblogs.com/haoxl/p/6818657.html*/
//(1)使用数组map()方法,对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组
var arr=[1,[2,[[3,4],5],6]];
function unid(arr){
var arr1=(arr + '').split(',');//将数组转字符串后再以逗号分隔转为数组
var arr2=arr1.map(function(x){
return Number(x);
});
return arr2;
}
console.log(unid(arr));
//(2)只适用于二维数组转一维数组
var arr=[1,[2,3],[4,5]];
arr1=[].concat.apply([],arr);
console.log(arr1);
/*18.鼠标滑过显示溢出的文本*/
function ellipsis(e) {
var target=e.target;
var containerLength=$(target).width();
var textLength=target.scrollWidth;
var text=$(target).text();
if (textLength > containerLength) {
$(target).attr("title", text);
}
}
$("#xxx").on("mouseenter", ellipsis)
*请认真填写需求信息,我们会在24小时内与您取得联系。