写在前面

  一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残。

  昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而构建vnode,构建指令,实现virtual dom,然后在这基础之上实现双向绑定等。【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

  今天博主就来详细的实现一个拥有核心功能的htmlParse函数,看看它内部的实现逻辑,了解它是怎么样去解析一个vue模板的。

小目标

  我们最终的目标是将html转换成ast对象,那么首先我们定一个小目标:

<div id="div1"></div>

  我希望将上面的html解析成ast格式,类似于下面:

{
"tag":"div",
"attrs":[
{ "id":"div1" }
],
"children":[],
"type":1
}

最终想要达成的第一个小目标是可以将div标签字符串输出成这样一个object格式,tag表示标签名称,attrs表示属性,children表示这个div所有的子节点,type的话表示节点的类型,我们今天只三个类型:

1.元素类型,也就是标签类型,所有用<tag attr=""></tag>这样的标签。2.变量text,现在我们实现一个{{text}}的变量转换,它其实是一个节点。3.普通文本,普通文本包括普通文字和空格、换行。

基本结构

  基本结构的设计决定的代码能扩展多远,如果一开始结构设计错误,最后在新加入的功能无法嵌入的时候,那就只有重构一条路可以走了。

  首先理清楚我们的思路。

  匹配单个字符》匹配标签》匹配属性》匹配文本》匹配结束标签

  然后,你想啊,html标签都是有开始,有结束的。那么这里问题就来了,可以想到的方式,解析一个标签的开始与结束吧,例如我们使用正则匹配开始标签<div id='div1'> 然后找到结束标签</div>,这样是不是就可以解析div里面的内容了?

难。

开始标签比较好找,结束标签就恶心了,例如 <div><div></div></div> ,,完了,怎么区分嵌套关系?第一个<div>到底匹配哪一个结束标签?

这个思路是错的,很难。

  那么我们换个思路,如果我们单个字符匹配呢,

 例如我们匹配一个 <div><div></div></div>,

ok 脑补步骤

 1。匹配到 <   匹配到这个字符我就可以认为,后面的要么是开始标签,要么是结束标签。

2。用正则匹配从<到后面的字符,如果是开始标签,现在记录一下,啊,我遇到了一个开始标签<div>  顺便用正则记录attrs

3. 现在我们匹配走走走。。。走到<div></div></div>

4.又匹配到一个 < 老步骤啊。

5.发现是开始标签,再次记录,啊,我又遇到一个开始标签  <div>  顺便用正则记录attrs

6. 现在我们匹配走走走。。。走到</div></div>

7. 又匹配到一个 < 老步骤啊。

8.发现是一个结束标签</div> ,嗯?结束标签!它是谁的结束标签?想一想。。。。。。应该是最后一个遇到的开始标签吧。  第一个遇到的结束标签不就是最后一个开始标签的结束么?

9.啊,结束了一个。

10.再匹配,再完成。

恩。。。思路清晰了有没有,来实现走一个:

  //转化HTML至AST对象
function parse(template){
var currentParent; //当前父节点
var root; //最终生成的AST对象
var stack = []; //插入栈
var startStack = []; //开始标签栈
var endStack = []; //结束标签栈
//console.log(template);
parseHTML(template,{
start:function start(targetName,attrs,unary,start,end,type,text){//标签名 ,attrs,是否结束标签,文本开始位置,文本结束位置,type,文本,
var element = { //我们想要的对象
tag:targetName,
attrsList:attrs,
parent:currentParent, //需要记录父对象吧
type:type,
children:[]
}
if(!root){ //根节点哈
root = element;
}
if(currentParent && !unary){ //有父节点并且不是结束标签?
currentParent.children.push(element); //插入到父节点去
element.parent = currentParent; //记录父节点
}
if (!unary) { //不是结束标签?
if(type == 1){
currentParent = element;//不是结束标签,当前父节点就要切换到现在匹配到的这个开始标签哈,后面再匹配到
startStack.push(element); //推入开始标签栈
}
stack.push(element); //推入总栈
}else{
endStack.push(element); //推入结束标签栈
currentParent = startStack[endStack.length-1].parent; //结束啦吧当前父节点切到上一个开始标签,这能理解吧,当前这个已经结束啦
}
//console.log(stack,"currentstack")
},
end:function end(){ },
chars:function chars(){ }
});
console.log(root,"root");
return root;
};
/**
* Not type-checking this file because it's mostly vendor code.
*/ /*!
* HTML Parser By John Resig (ejohn.org)
* Modified by Juriy "kangax" Zaytsev
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*/ // Regular Expressions for parsing tags and attributes
var singleAttrIdentifier = /([^\s"'<>/=]+)/;
var singleAttrAssign = /(?:=)/;
var singleAttrValues = [
// attr value double quotes
/"([^"]*)"+/.source,
// attr value, single quotes
/'([^']*)'+/.source,
// attr value, no quotes
/([^\s"'=<>`]+)/.source
];
var attribute = new RegExp(
'^\\s*' + singleAttrIdentifier.source +
'(?:\\s*(' + singleAttrAssign.source + ')' +
'\\s*(?:' + singleAttrValues.join('|') + '))?'
);
// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
// but for Vue templates we can enforce a simple charset
var ncname = '[a-zA-Z_][\\w\\-\\.]*';
var qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')';
var startTagOpen = new RegExp('^<' + qnameCapture);
var startTagClose = /^\s*(\/?)>/;
var endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>');
var doctype = /^<!DOCTYPE [^>]+>/i;
var comment = /^<!--/;
var conditionalComment = /^<!\[/; //偷懒哈 上面的正则是我在vue上拿下来的,这个后期可以研究,下面的话简单的写两个用用,和vue原版的是有一些差别的 //{{变量}} var varText = new RegExp('{{' + ncname + '}}');
//空格与换行符
var space = /^\s/;
var checline = /^[\r\n]/;
/**
type 1普通标签
type 2代码
type 3普通文本
*/
function parseHTML(html,options){
var stack = []; //内部也要有一个栈
var index = 0; //记录的是html当前找到那个索引啦
var last; //用来比对,当这些条件都走完后,如果last==html 说明匹配不到啦,结束while循环
var isUnaryTag = false; while(html){
last = html;
var textEnd = html.indexOf('<');
if(textEnd === 0){ //这一步如果第一个字符是<那么就只有两种情况,1开始标签 2结束标签
//结束标签
var endTagMatch = html.match(endTag); //匹配
if(endTagMatch){
console.log(endTagMatch,"endTagMatch");
isUnaryTag = true;
var start = index;
advance(endTagMatch[0].length); //匹配完要删除匹配到的,并且更新index,给下一次匹配做工作
options.start(null,null,isUnaryTag,start,index,1);
continue;
}
//初始标签
var startMatch = parseStartTag();
if(startMatch){
parseStartHandler(startMatch);//封装处理下
console.log(stack,"startMatch");
continue;
}
} if(html === last){
console.log(html,"html");
break;
}
}
function advance (n) {
index += n;
html = html.substring(n);
}
//处理起始标签 主要的作用是生成一个match 包含初始的attr标签
function parseStartTag(){
var start = html.match(startTagOpen);
if(start){
var match = {
tagName: start[1], // 标签名(div)
attrs: [], // 属性
start: index // 游标索引(初始为0)
};
advance(start[0].length);
var end, attr;
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {//在endClose之前寻找attribute
advance(attr[0].length);
match.attrs.push(attr);
}
if (end) {
advance(end[0].length); // 标记结束位置
match.end = index; //这里的index 是在 parseHTML就定义 在advance里面相加
return match // 返回匹配对象 起始位置 结束位置 tagName attrs
} }
}
//对match进行二次处理,生成对象推入栈
function parseStartHandler(match){
var _attrs = new Array(match.attrs.length);
for(var i=0,len=_attrs.length;i<len;i++){ //这儿就是找attrs的代码哈
var args = match.attrs[i];
var value = args[3] || args[4] || args[5] || '';
_attrs[i] = {
name:args[1],
value:value
}
}
stack.push({tag: match.tagName,type:1, lowerCasedTag: match.tagName.toLowerCase(), attrs: _attrs}); //推栈
options.start(match.tagName, _attrs,false, match.start, match.end,1); //匹配开始标签结束啦。
} }

我们执行   parse("<div id='test1'><div></div></div>");  大功告成哈哈哈哈哈  呃。

神马,你还想问我细节问题?

正好给你培养一下读代码的能力哈,思路有了,代码有了,拉下去调试调试哈。当然博主在下一章还会详细介绍的。

写在后面

  mvvm框架和webpack的出现确实改变了前端的开发方式,使得学习前端变成了一门有着深入学问的课题。在我们日常开发中应该不断地学习,归纳,总结,寻找新的思想,对原有的代码有好的补充和好的改进。

写的不好,谢谢大家观看。 后续有空会新增更多关于开发的知识分享。

如果你有什么疑问,你可以联系我,或者在下方评论。

【vuejs深入三】vue源码解析之二 htmlParse解析器的实现的更多相关文章

  1. vue源码实现的整体流程解析

    一.前言 最近一直在使用vue做项目,闲暇之余查阅了一些关于vue实现原理的资料,一方面对所了解到的知识做个总结,另外一方面希望能对看到此文章的同学有所帮助.本文如有不足之处,还请过往的大佬批评指正. ...

  2. vue源码cached高阶函数解析

    1.源代码 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <tit ...

  3. 【Vuejs】350- 学习 Vue 源码的必要知识储备

    前言 我最近在写 Vue 进阶的内容.在这个过程中,有些人问我看 Vue 源码需要有哪些准备吗?所以也就有了这篇计划之外的文章. 当你想学习 Vue 源码的时候,需要有扎实的 JavaScript 基 ...

  4. vue源码阅读(二)

    一 一个实例 如果简单了解过些Vue的API的话,肯定会对一下这个特别熟悉,在上一篇里,分析了Vue的核心文件core的index.js构造vue函数执行的流程. 那么下边这个则是实例化构造函数,也就 ...

  5. Vue源码学习(二)——生命周期

    官网对生命周期给出了一个比较完成的流程图,如下所示: 从图中我们可以看到我们的Vue创建的过程要经过以下的钩子函数: beforeCreate => created => beforeMo ...

  6. 【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

    写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. vuejs是一个优秀的前端mvvm框架,它的易用性和渐进式的理念可以使每一个前端开发人员感到舒服,感到easy.它内 ...

  7. Vue源码解析(一):入口文件

    在学习Vue源码之前,首先要做的一件事情,就是去GitHub上将Vue源码clone下来,目前我这里分析的Vue版本是V2.5.21,下面开始分析: 一.源码的目录结构: Vue的源码都在src目录下 ...

  8. Vue源码解析---数据的双向绑定

    本文主要抽离Vue源码中数据双向绑定的核心代码,解析Vue是如何实现数据的双向绑定 核心思想是ES5的Object.defineProperty()和发布-订阅模式 整体结构 改造Vue实例中的dat ...

  9. Vue源码解析之数组变异

    力有不逮的对象 众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式.当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变. 这是什么原因? 原因在于: Vue 的响应式 ...

随机推荐

  1. C#日志文件

    写日志文件是一个很常用的功能,以前都是别人写好的,直接调用的,近期写了一个小工具,因为比较小,所以懒得引用dll文件了,直接上网找了一个,很方便,现在记录下 public class LogClass ...

  2. Python模块 - paramiko

    paramiko模块提供了ssh及sft进行远程登录服务器执行命令和上传下载文件的功能.这是一个第三方的软件包,使用之前需要安装. 1 基于用户名和密码的 sshclient 方式登录 # 建立一个s ...

  3. python实现 字符串匹配函数

    通配符是 shell 命令中的重要功能,? 表示匹配任意 1 个字符,*表示匹配 0 个或多个字符.请使用你熟悉的编程语言实现一个字符串匹配函数,支持 ? 和 * 通配符.如 "a?cd*d ...

  4. Spring(五):Spring&Struts2&Hibernate整合后,实现查询Employee信息

    背景: 基于之前两篇文章<Spring(三):Spring整合Hibernate>.<Spring(四):Spring整合Hibernate,之后整合Struts2>,了解了如 ...

  5. hdu 6095 Rikka with Competition---思维题贪心

    题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=6095 题目大意: 任意两个人相比,相差大于K,分低的淘汰,否则两人都有可能赢,剩下的继续比,问有最多 ...

  6. 【转】C++ STL快速入门

    转自:https://www.cnblogs.com/skyfsm/p/6934246.html 冠军的试炼 悟已往之不谏,知来者之可追 博客园 首页 新随笔 联系 订阅 管理 随笔 - 60  文章 ...

  7. [LeetCode] Couples Holding Hands 两两握手

    N couples sit in 2N seats arranged in a row and want to hold hands. We want to know the minimum numb ...

  8. 【django之admin,单例模式】

    一.admin组件使用 Django 提供了基于 web 的管理工具. Django 自动管理工具是 django.contrib 的一部分.你可以在项目的 settings.py 中的 INSTAL ...

  9. 深入解析 SQL Server 高可用镜像实现原理

    作者:郭忆 本文由 网易云 发布. SQL Server 是 windows 平台 .NET 架构下标配数据库解决方案,与 Oracle.MySQL 共同构成了 DB-Engines Ranking ...

  10. spark-shell报错:Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/hadoop/fs/FSDataInputStream

    环境: openSUSE42.2 hadoop2.6.0-cdh5.10.0 spark1.6.0-cdh5.10.0 按照网上的spark安装教程安装完之后,启动spark-shell,出现如下报错 ...