写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧
【Vue原理】Compile - 源码版 之 Parse 主要流程
本文难度较繁琐,需要耐心观看,如果你对 compile 源码暂时不感兴趣可以先移步白话版 Compile - 白话版,
parse 是 渲染三巨头的老大,其作用是把 template 字符串模板,转换成 ast
其涉及源码也是多得一批,达到了 一千多行,想想如果我把全部源码放到文章里面来简直不能看,所以我打算只保留主要部分,就是正常流程可以走通,去掉那些特殊处理的地方
大部分源码都是特殊处理,比如 script ,style,input ,pre 等标签,这次全部都去掉,只留下通用元素的处理流程,留下一个骨架
因为 parse 的内容非常的多,除了精简源码之外,我还通过不同内容划分文章去记录
今天,要记录的就是 parse 解析 template 成 ast 的大致流程,而怎么解析标签名,怎么解析标签属性会暂时忽略,而独立成文。当有解析标签名和解析属性的地方会直接出结果。比如当我说在 模板 "
" 匹配出头标签时,直接就得到 div ,而不会去考究是如何匹配出来的好的,到底 template 是怎么变成 ast 的呢?跟着我去探索把~
AST先来说说 ast 吧,这种复杂的概念,反正是需要查的。所以本文根本不需要解释太多
直接说我的理解吧
抽象语法树,以树状形式表现出语法结构
直接使用例子去直观感受就好了
111
用 ast 去描述这个模板就是
{ tag:"div", type :1 , children:[ { type:3, text:"11" } ] }
简单得一批把,复杂的这里也不提了,反正跟 parse 没多大关系我觉得
另外记一下,节点的 type 表示的意思
type:1,节点
type:2,表达式,比如 {{isShow}}
type:3,纯文本
现在就开始 parse 的内容了,那么就看 parse 的源码
Parseparse 是渲染三巨头的老大,同时它也是一个函数,源码如下
function parse(template) { var stack = []; // 缓存模板中解析的每个节点的 ast var root; // 根节点,是 ast var currentParent; // 当前解析的标签的父节点 /** * parseHTML 处理 template 匹配标签,再传入 start,end,chars 等方法 **/ parseHTML(template, { start: (..被抽出,在后面) end: (..被抽出,在后面), // 为 起始标签 开启闭合节点 chars: (..被抽出,在后面) // 文字节点 }); return root }
parse 接收 template 字符串,使用 parseHTML 这个函数在 template 中匹配标签
并传入 start,end,chars 三个函数 供 parseHTML 处理标签等内容
start,end,chars 方法都已经被我抽出来,放在后面逐个说明
下面来看下其中声明的三个变量
1 stack是一个数组存放模板中按顺序 从头到尾 每个标签的 ast
注:不会存放单标签的 ast ,比如 input,img 这些
比如 stack 是这样的
stack=[{ tag:"div", type :1 , children:[ { type:3, text:"11" } ] }]
主要作用是帮助理清节点父子关系
2 root每个模板都必须有一个根节点。写过 Vue 项目的都知道了,所以一般解析到第一个标签的时候,会直接设置这个标签为 根节点
并且最后返回的也是 root
不可以存在两个根节点(有 v-if 的不讨论)
3 currentParent在解析标签的时候,必须要知道这个标签的 父节点时谁
这样才知道 这个标签是谁的子节点,才能把这个节点添加给相应的 节点的 children
注:根节点 没有 父节点,所以就是 undefined
parse 源码已经被我精简得很简单了,主要内容其实就在 其中涉及的四个方法中
parseHTML,start,end,chars
parseHTML 是处理 template 的主力,其他三个函数是功能类型的,负责处理相应的内容。 例如,start 是处理头标签的,end 是处理尾标签的,chars 是处理文本的
先来看看 parseHTML
处理 templateparseHTML 作为处理 template,匹配标签的函数,是十分庞大的,其中兼顾了非常多情况的处理
而本次在不影响流程的情况下,我去掉了下面这些处理,优化阅读
1、没有结束标签的处理
2、文字中包含 < 的处理
3、注释的处理
4、忽略首尾空白字符,默认起始和结尾都是标签
个人认为主要内容为三个
1、循环 template 匹配标签
2、把匹配到的内容,传给相应的方法处理
3、截断 template
来看源码,已经简化得不行了,但是还是要花点心思看看
function parseHTML(html, options) { while (html) { // 寻找 < 的起始位置 var textEnd = html.indexOf("<"), text ,rest ,next; // 模板起始位置是标签开头 < if (textEnd === 0) { /** * 如果是尾标签的 < * 比如 html = "
思路如下
1匹配 < 这个符号因为他是标签的开头(已经排除了文字中含有 < 的处理,不做讨论)
2如果 template 开头是 <那么可能是 尾标签,可能是 头标签,那么就需要判断到底是哪个
1、先匹配尾标签,如果匹配到,那么就是尾标签,使用 end 方法处理。
2、如果不是,使用 parseStartTag 函数匹配得到首标签,并把 首标签信息传给 start 处理
parseStartTag 就是使用正则在template 中匹配出 首标签信息,其中包括标签名,属性等
比如 template 是
html = "111;"
parseStartTag 处理匹配之后得到
{ tagName: "div", attrs: [{name:"22"}] }3 如果 template 开头不是 <
那么证明 开头 到 < 的位置这一段,是字符串,那么就是文本了
传给 chars 方法处理
每次处理一次,就会截断到匹配的位置,然后 template 越来越短,直接为空,退出 while,于是处理完毕
对于截断呢,使用 substring,可能忘了怎么作用的,写个小例子
传入数字,表示这个位置前面的字符串都不要
然后,就到了我们其他三个方法的闪亮登场了
处理头标签每当 parseHTML 匹配到一个 首标签,都会把该标签的信息传给 start 方法,让他来处理
function start(tag, attrs, unary) { // 创建 AST 节点 var element = createASTElement(tag, attrs, currentParent); /** * ...省略了一段处理 vFor,vIf,解析 @ 等属性指令的代码 **/ // 设置根节点,一个模板只有一个根节点 if (!root) root = element; // 处理父子关系 if (currentParent) { currentParent.children.push(element); element.parent = currentParent; } // 不是单标签(input,img 那些),就需要保存 stack if (!unary) { currentParent = element; stack.push(element); } }
精简得一目了然(面目全非),看得极度舒适
看看 start 方法都做了哪些恶呢
1、创建 ast
2、解析 attrs,并存放到 ast (已省略属性解析)
3、设置根节点,父节点,把节点添加进父节点的 children
4、ast 保存进 stack
好像不用解释太多,肯定都看得懂啊,除了一个 创建 ast 的函数
这就来源码
function createASTElement(tag, attrs, parent) { return { type: 1, tag: tag, attrsList: attrs, // 把 attrs 数组 转成 对象 attrsMap: makeAttrsMap(attrs), parent: parent, children: [] } }
创建一个 ast 结构,保存数据
直接返回一个对象,非常明了,包含的各种属性,应该也能看懂
其中有一个 makeAttrsMap 函数,举个栗子
模板上的属性,经过 parseHTML 解析成一个数组,如下
[{ name:"hoho" ,value:"333" },{ name:"href" ,value:"444" }]
makeAttrMap 转成对象成这样
{ hoho:"333", href:"444"}
然后就保存在 ast 中
处理尾标签每当 parseHTML 匹配到 尾标签 ,比如 "
来看看吧
function end() { // 标签解析结束,移除该标签 stack.length -= 1; currentParent = stack[stack.length - 1]; }
乍一看,很简单啊!这么少(都是精简...)
作用有两个
1从 stack 数组中移除这个节点stack 保存的是匹配到的头标签,如果标签已经匹配结束了,那么就需要移除
stack 就是为了明确各节点间父子关系而存在的
保证 stack 中最后一个节点,永远是下次匹配的节点的父节点
举个栗子,存在下面模板
stack 匹配两个 头标签之后
stack = [ "div" , "section"]
看看 start 可以知道,此时 currentParent = section
然后匹配到 ,则移除 stack 中的 section,并且重设 currentParent
stack = ["div"] currentParent = "div"
再匹配到 p 的时候,p 的父节点就是 div,父子顺序就是正确的了
2重新设置 stack 最后一个节点为父节点 处理文本字符串当 parseHTML 去匹配 < 的时候,发现 template 不是 <,template开头 到 < 还有一段距离
那么这段距离的内容就是 文本了,那么就会把这段文本传给 chars 方法处理
来看看源码
function chars(text) { // 必须存在根节点,不可能用文字开头 if(!currentParent) return var children = currentParent.children; // 通过 parseText 解析成字符串,判断是否含有双括号表达式,比如 {{item}} // 如果是有表达式,会存放多一些信息, var res = parseText(text) if(res) { children.push({ type: 2, expression: res.expression, tokens: res.tokens, text: text }); } // 普通字符串,直接存为 字符串子节点 else if( !children.length || children[children.length - 1].text !== " " ) { children.push({ type: 3, text: text }); } }
这段代码主要作用就是,为 父节点 添加 文本子节点
而文本子节点分为两种类型
1、普通型,直接存为文本子节点
2、表达式型,需要经过 parseText 处理
直接以结果来定义吧
比如处理这段文本
{{isShow}}
{ expression: toString(isShow) tokens: [{@binding: "isShow"}] }
主要是为了把表达式 isShow 拿到,方便后面从实例上获取值
好的,现在,template 处理流程所涉及的主要方法都讲完了
现在用上面这些函数来走一个流程
现在有一个模板
1 开始循环 tempalte11
匹配到第一个 头标签 (
该 div 的 ast 变成根节点 root,并设置其为当前父节点 currentParent,保存进节点缓存数组 stack
此时
stack = [ { tag:"div" , children:[ ] } ]
第一轮处理结束,template 截断到第一次匹配到的位置
此时,template = 11
开始匹配 <,发现 < 不在开头,而 开头位置 到 < 有一段普通字符串
调用 parse-char,传入字符串
发现其没有 双括号表达式,直接给父节点添加简单子节点
currentParent.children.push({ type:3 , text:"11" })
此时
stack =[ { tag:"div" , children:[ { type:3 , text:"11" } ] } ]
第二轮处理结束,template 截断到刚刚匹配完的字符串
此时,template =
继续寻找 <,发现就在开头,但是这是一个结束标签,标签名是 div
因为 stack 是节点顺序存入的,这个结束标签肯定属于 stack 最后一个 标签
由于 该标签匹配完毕,所以从 stack 中移除
并且设置 当前父节点 currentParent 为 stack 倒数第二个
第三次遍历结束,template 继续截断
此时 template 为空了,结束所有遍历
返回此次 tempalte 解析的 root
{ tag:"div",type :1 , children:[ { type:3 , text:"11" } ] }
于是 parse 就成功把 tempalte 解析成了 ast ,就是 root
总结本问讲的是 parse 的主要流程,忽略了内部的处理细节,比如怎么解析标签,怎么解析属性,其他内容都会独立成文章
在 parse 的流程中,大致有五个函数,我们屡一下,如下
parse,parseHTML,start,end,chars
parse 是整个 parse 流程的总函数
parseHTML 是 parse 处理的主力函数
start,end,chars 是 在 parse 中传给 parseHTML ,用来帮助处理 匹配的标签信息的函数,这三个函数会在 parseHTML 中被调用
最后鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/106593.html
摘要:当字符串开头是时,可以匹配匹配尾标签。从结尾,找到所在位置批量闭合。 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧 【Vue原理】Compile - 源码版 之 标签解析...
摘要:页面这个实例,按理就需要解析两次,但是有缓存之后就不会理清思路也就是说,其实内核就是不过是经过了两波包装的第一波包装在中的内部函数中内部函数的作用是合并公共和自定义,但是相关代码已经省略,另一个就是执行第二波包装在中,目的是进行缓存 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 ...
摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之属性解析哈哈哈,今天终 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...
摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理白话版终于到了要讲白话的时候了 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...
摘要:还原的难度就在于变成模板了,因为其他的什么等是原封不动的哈哈,可是直接照抄最后鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版...
阅读 1640·2019-08-30 15:44
阅读 2564·2019-08-30 11:19
阅读 391·2019-08-30 11:06
阅读 1555·2019-08-29 15:27
阅读 3075·2019-08-29 13:44
阅读 1620·2019-08-28 18:28
阅读 2351·2019-08-28 18:17
阅读 1977·2019-08-26 10:41