text/template 代码阅读: parse

版权

本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.youkuaiyun.com/big_cheng/article/details/126039101.

lex.go

lexer 负责分词(拆分token).

流程

func (l *lexer) run() {
	for state := lexText; state != nil; {
		state = state(l)
	}
	......
}

给定一段模板源码, 例如:

para1
{{if .a}} para2 {{end}}

lexer 首先进入plain text 解析:

func lexText(l *lexer) stateFn {
	l.width = 0
	if x := strings.Index(l.input[l.pos:], l.leftDelim); x >= 0 {
		......
		if l.pos > l.start {
			......
			l.emit(itemText)
		}
		......
		return lexLeftDelim
	}
	......
}

从当前pos(初值0, 开头) 开始找模板action 的开始分隔符(“{{”). 如果找到, 先将"{{" 前的itemText 发给分词接收者(一般就是parser), 然后返回lexLeftDelim - 该函数进入action 开始的处理.

lexLeftDelim()

func lexLeftDelim(l *lexer) stateFn {
	......
	if strings.HasPrefix(l.input[l.pos+afterMarker:], leftComment) {
		......
		return lexComment
	}
	l.emit(itemLeftDelim)
	......
	return lexInsideAction
}

action 可能是comment ( {{/* xxx */}} ), 返回lexComment 去处理.
否则是pipeline/control action ( {{.a}}, {{if .a}} … {{end}}, {{print 1}} 等 ), 先发送"{{", 再返回lexInsideAction.

lexInsideAction()

func lexInsideAction(l *lexer) stateFn {
	......
	switch r := l.next(); {
	......
	case r == '"':
		return lexQuote
	......
	case r == '\'':
		return lexChar
	......
	}
	return lexInsideAction
}		

模板action 有规定的写法. 例如遇到双引号(“) 代表开始一个字符串常量( {{print “xxx”}} ). 不可能遇到字符常量里的双引号( {{print '”'}} ), 因为先遇到字符常量分隔符(单引号, ') 时就已经转到lexChar 处理.

如上, lexer 根据特定分隔符判断要处理何种item, 同时发送中间结果item. 如此直到返回nil stateFn.

space 与trimMarker

见包文档, 模板支持action 的前、后trim. 如:

"{{23 -}} < {{- 45}}"

“23” 后的" -}}" 指示去掉action 后的空格. “45” 前的"{{- " 指示去掉action 前的空格. 输出结果将是:

"23<45"

action 里(首次) 遇到空格时:

func lexInsideAction(l *lexer) stateFn {
	......
	switch r := l.next(); {
	......
	case isSpace(r):
		l.backup()
		return lexSpace
	......
	}
	......
}

backup() 退回到空格之前(每next() 一次只能backup() 一次), 转到lexSpace 处理.

func lexSpace(l *lexer) stateFn {
	var r rune
	var numSpaces int
	for {
		r = l.peek()
		if !isSpace(r) {
			break
		}
		l.next()
		numSpaces++
	}
	if hasRightTrimMarker(l.input[l.pos-1:]) && strings.HasPrefix(l.input[l.pos-1+trimMarkerLen:], l.rightDelim) {
		l.backup()
		if numSpaces == 1 {
			return lexRightDelim
		}
	}
	l.emit(itemSpace)
	return lexInsideAction
}

peek() 不消费next rune. 由于仅lexInsideAction() 会调用lexSpace(), 所以numSpaces >= 1.
退出for 循环时pos 在空格之后第一个非空格字符. 如果是单个空格 + “-}}”, 退回空格 - lexRightDelim() 会处理trim 及"}}“. 如果是多个空格 + “-}}”, 先退回一个空格, 将前面的一到多个空格合并发送为一个itemSpace, 之后再转到lexInsideAction() 处理时就变成单个空格 + “-}}” 的情况. 如果不是在”}}" 边界处, 则合并发送一个itemSpace.

identifier

func lexInsideAction(l *lexer) stateFn {
	......
	switch r := l.next(); {
	......
	case r == '+' || r == '-' || ('0' <= r && r <= '9'):
		l.backup()
		return lexNumber
	case isAlphaNumeric(r):
		l.backup()
		return lexIdentifier

action 里如果以0-9开头则转到lexNumber 处理, 所以下一个case isAlphaNumeric 实际仅以字母开头. 回退刚读到的字符, 转到lexIdentifier.

func lexIdentifier(l *lexer) stateFn {
Loop:
	for {
		switch r := l.next(); {
		case isAlphaNumeric(r):
			// absorb.
		default:
			l.backup()
			......
			if !l.atTerminator() {
				return l.errorf("bad character %#U", r)
			}

首次l.next() 读到刚才回退的(首)字符, 后续的字母数字一直读取, 直到不是字母数字(回退它).

func (l *lexer) atTerminator() bool {
	r := l.peek()
	if isSpace(r) {
		return true
	}
	switch r {
	case eof, '.', ',', '|', ':', ')', '(':
		return true
	}
	if rd, _ := utf8.DecodeRuneInString(l.rightDelim); rd == r {
		return true
	}
	return false
}

identifier 只能后跟空格、eof/dot/逗号/竖线/冒号/左右括号, 或者action 结束分隔符的首字符, 其他的非字母数字均不允许.
atTerminator() 也被lexField、lexVariable 调用, 条件放宽了. 例如:

{{print(1)}}

分词是identifier “print” 和左括号"(“, 但parser 处理时将报错(在command(). operand 即"print” 只能后跟空格、}}、右括号、竖线).
另, identifier 后跟action 结束分隔符的首字符也是允许的, 例如:

{{$x:=1}}{{$x}2}}

同样, 也将在后面parser 处理时报错( unexpected “}” in operand ).

pipeline, command, operand, term

语法上, 模板里"{{ }}" 包围的是action, action 之外是plain text(html/css/js 等).
action (除了comment action 之外) 分2种:

  • control action 如: {{if pipeline}}xx{{end}}、{{range pipeline}}xx{{else}}yy{{end}}
  • {{pipeline}}

pipeline 是一个或用管道符号(竖线, |) 分隔的多个command. 单个command 的例子:

  • 单个常量/变量/字段/key 等: 1、“abc”、nil、.、$x、.Field1、.k1
  • 串联的一组字段/key: .Field1.k1.Field2.k2
  • 当前dot 的一个方法调用: .Method1. 方法调用可以串联如: $x.Method1.Field1.k1.Method2
  • 单个函数调用(函数分全局和模板自定义 2层) 如: func1
  • 函数和串联末尾的方法可以带参数(空格分隔) 如: $x.Method1 “arg1” “arg2”、print 1 2 3
  • 以上均可用括号包围, 视为单个. 例如print 的参数是函数调用的结果: print (print “abc”). 例如方法调用的结果再串联: (.Method1 “abc”).Field1

command 是单个或空格分隔的多个operand.
operand 是一个term, 后面可以串联一到多个.FieldXx.
term 是单个常量/变量/函数/字段等, 如1、nil、true、print、.Field1、$.
term 也可以是括号包围的一个pipeline.

parse.go

parser 持续读取lexer 拆出的分词(item, 即token), 构建node.go 里定义的各节点(XxNode). 例如:

{{if .a}} {{.b}} {{else}} {{.c}}{{.d}} {{end}}

会构建出一个IfNode:

type IfNode struct {
	BranchNode
}

type BranchNode struct {
	NodeType
	Pos
	tr       *Tree
	Line     int
	Pipe     *PipeNode
	List     *ListNode
	ElseList *ListNode
}

Pipe 对应pipeline “.a”. List 含单个ActionNode, 对应".b". ElseList 含2个ActionNode, 分别对应".c" 和".d".

t.parse()

parse() 是解析模板内容的入口方法, 除了解析内容里的plain text 和action 之外, 也解析(关联)模板定义如"{{define “tpl-1”}}xxx{{end}}" (关联模板只能定义在内容的顶层):

func (t *Tree) parse() {
	t.Root = t.newList(t.peek().pos)
	for t.peek().typ != itemEOF {
		if t.peek().typ == itemLeftDelim {
			delim := t.next()
			if t.nextNonSpace().typ == itemDefine {
				newT := New("definition")
				newT.text = t.text
				newT.Mode = t.Mode
				newT.ParseName = t.ParseName
				newT.startParse(t.funcs, t.lex, t.treeSet)
				newT.parseDefinition()
				continue
			}
			t.backup2(delim)
		}
		switch n := t.textOrAction(); n.Type() {
		case nodeEnd, nodeElse:
			t.errorf("unexpected %s", n)
		default:
			t.Root.append(n)
		}
	}
}

持续解析, 如果下一token 是"{{" 且再下一个非空格token 是"define" (实际是identifier, 但lexer 专门定义为类型itemDefine), 则定义一个新的parser - New(“definition”) - 去解析它.
由于newT.startParse() 参数"t.lex", 实际是接着当前分词的位置 - 即"define" 之后 - 解析.

func (t *Tree) parseDefinition() {
	const context = "define clause"
	name := t.expectOneOf(itemString, itemRawString, context)
	var err error
	t.Name, err = strconv.Unquote(name.val)
	if err != nil {
		t.error(err)
	}
	t.expect(itemRightDelim, context)
	
	var end Node
	t.Root, end = t.itemList()
	if end.Type() != nodeEnd {
		t.errorf("unexpected %s in %s", end, context)
	}
	t.add()
	t.stopParse()
}

当前词法位置(pos) 在"define" 之后, t.expectOneOf() 限制后面只能是itemString 或itemRawString - 即关联模板名如 {{define “tpl-1”.
strconv.Unquote() 解析得到关联模板名t.Name - 此实际名称会代替上面parse() 里New(“definition”) 里的临时名.
留意parser 通常忽略action 内部做分隔用的itemSpace, 例如"define" 和"tpl-1" 之间的(一到多个)空格仅被lexer 用于词法分隔, parser 并不关心.

t.itemList() 解析关联模板的定义(含结尾"{{end}}").

func (t *Tree) add() {
	tree := t.treeSet[t.Name]
	if tree == nil || IsEmptyTree(tree.Root) {
		t.treeSet[t.Name] = t
		return
	}
	if !IsEmptyTree(t.Root) {
		t.errorf("template: multiple definition of template %q", t.Name)
	}
}

t.treeSet 包含t 的所有关联模板(名称=>*Tree), 也包括自己. 例如一个模板对象名称为"foo", 定义内容是"xxx {{define “bar”}}yy{{end}}“, 则该对象的treeSet 包含"foo” 和"bar" 2项.
t.add() 会检查不重名. 但是已有的重名模板如果没有实际内容, 则可以被覆盖.

完成t.parseDefinition() 回到t.parse(): 后面是continue 即检查{{define “xx”}}…{{end}} 后面的token(可以是空格 - itemText).
如果消费了"{{" 再后面不是"define", 此时parser 内部分词缓存(Tree.token [3]item) 第0项是该不是"define" 的token. t.backup2(delim) 后缓存内容是: 非"define"、{{ 2项。缓存是后进先出, 所以下次t.next() 取到"{{“, 再下次t.next() 取到非"define”. 即目前消费了2个token 但是发现用不上, 还回去.

t.parse() 继续: 如果下一个token 不是"{{“, 或者下2个连续token 不是”{{define", 则调用t.textOrAction() 处理 - 可能是plain text 或action.

t.textOrAction()

func (t *Tree) textOrAction() Node {
	switch token := t.nextNonSpace(); token.typ {

留意lexer itemSpace 仅指action 之内(分隔)的空格. action 之外的所有文本(可包含空格) 才是plain text (itemText).
t.textOrAction() 处理plain text、comment action、其他action.

elseif

if action 可有任意多个elseif:

{{if .a}} 1 {{else if .b}} 2 {{else if .c}} 3 {{else}} 4 {{end}}

如前, IfNode:

type IfNode struct {
	BranchNode
}

type BranchNode struct {
	......
	List     *ListNode
	ElseList *ListNode
}

解析:

func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
	......
	pipe = t.pipeline(context, itemRightDelim)
	......
	list, next = t.itemList()
	......
	switch next.Type() {
	case nodeEnd:
	case nodeElse:
		if allowElseIf {
			if t.peek().typ == itemIf {
				t.next()
				elseList = t.newList(next.Position())
				elseList.append(t.ifControl())
				break
			}
		}
		elseList, next = t.itemList()
		if next.Type() != nodeEnd {
			t.errorf("expected end; found %s", next)
		}
	}
	return pipe.Position(), pipe.Line, pipe, list, elseList
}

进入t.parseControl() 时"if" 已消费. 先处理pipeline, 再处理{{if}} 后面的itemList - 消费到{{else}} 或{{end}}.
如果到{{else}} 且allowElseIf 且下一个token 是"if", 则消费if 后再次调用t.ifControl() 即t.parseControl(). 如此每一次处理else if 都递归, 直到最后的{{else if}} {{else}} {{end}} 处理返回一个IfNode:

{{if .a}} 1 {{else if .b}} 2 {{else if }} + IfNode

它作为对应{{ if .b}} 的IfNode 的elseList 部分, 如此递归逐一返回. 即最终返回的每个(嵌套)IfNode 的ElseList 都只包含单一IfNode:

IfNode(.a)
    list(1)
    elseList
        IfNode(.b)
            list(2)
            elseList
                IfNode(.c)
                    list(3)
                    elseList(4)

else 与if 之间的空格

上面仔细看有一个疑问: 如果itemList 以"else" 结束且allowElseIf 且下一个token 是"if", 但实际上else 的下一个token 是空格而非if?
经实际调试, 发现关键在else 的处理. 对于:

{{if .a}} 1 {{else if

t.parseControl() 里t.itemList() 会消费"1 {{else":

func (t *Tree) itemList() (list *ListNode, next Node) {
	list = t.newList(t.peekNonSpace().pos)
	for t.peekNonSpace().typ != itemEOF {
		n := t.textOrAction()

" 1 " 整个作为itemText 被t.textOrAction() 处理:

func (t *Tree) textOrAction() Node {
	switch token := t.nextNonSpace(); token.typ {
	case itemText: // lexer itemText: " 1 "
		return t.newText(token.pos, token.val)
	case itemLeftDelim:
		t.actionLine = token.line
		defer t.clearActionLine()
		return t.action()

t.action() 处理"{{else " (“{{” 已消费) :

func (t *Tree) action() (n Node) {
	switch token := t.nextNonSpace(); token.typ {
	......
	case itemElse:
		return t.elseControl()

“else” 已消费:

func (t *Tree) elseControl() Node { // {{if .a}} 1 {{else if
	peek := t.peekNonSpace() // "if"
	if peek.typ == itemIf {
		return t.newElse(peek.pos, peek.line)
	}
	token := t.expect(itemRightDelim, "else") // 如果不是{{else if", 则必须是{{else}}
	return t.newElse(token.pos, token.line)
}

func (t *Tree) peekNonSpace() item {
	token := t.nextNonSpace()
	t.backup()
	return token
}

t.peekNonSpace() 是一直消费itemSpace 直到itemIf, 然后t.backup() 退还itemIf: 即"if" 前的空格实际已被t.elseControl() 消费, 所以"else " 之后peek 到的是itemIf.
另留意, ElseNode 的pos 实际使用其后面的"if" 或"}}" 的pos.

t.pipeline() 里的backup2/3

func (t *Tree) pipeline(context string, end itemType) (pipe *PipeNode) {
	token := t.peekNonSpace()
	pipe = t.newPipeline(token.pos, token.line, nil)
decls:
	if v := t.peekNonSpace(); v.typ == itemVariable {
		t.next()
		// 3-token look-ahead
		tokenAfterVariable := t.peek()
		next := t.peekNonSpace()
		switch {
		case next.typ == itemAssign, next.typ == itemDeclare:
			......
		case next.typ == itemChar && next.val == ",":
			......
		case tokenAfterVariable.typ == itemSpace:
			t.backup3(v, tokenAfterVariable)
		default:
			t.backup2(v)
		}
	}

进入t.pipeline() 如以变量开头, 例如"{{$x 2" ( {{ 已消费 ), 需多读取一个非空格token 以区分赋值( “{{$ =” ), 这样包括空格需要读取3个token.
除了定义({{$x :=)、赋值({{$x =)、双变量({{$k, $v), 其他情况不处理:

  • 如果是后跟空格如"{{$x 2": token缓存第0项是"2", t.backup3() 后3个token 缓存项依次是"2"、空格、$x
  • 否则例如"{{$x/“: t.backup2() 后2个token 缓存项依次是”/"、$x
<template> <view> <Navbar title="八股文详情"> </Navbar> <ShowTips></ShowTips> <SlidePage @loadData="loadDataNext"> <view class="question-detail"> <view class="title-info"> <QuestionItem :data="questionData" :showDetail="false"></QuestionItem> </view> <view class="question" v-if="questionData.question"> <view class="question-title">问题描述</view> <uvParse class="rich-text" :content="questionData.question" :container-style="containerStyle" ></uvParse> </view> <view class="line"></view> <view class="question-content" v-if="readMode === '1' || (readMode === '0' && showAnswer)" > <view class="title">-<text class="inner">问题解析</text>-</view> <uvParse class="rich-text" :content="questionData.answerAnalysis" v-if="questionData.answerAnalysis" :container-style="containerStyle" ></uvParse> </view> </view> </SlidePage> <Footer :objectId="questionData.questionId + ''" :collectType="1" @updateCollect="updateCollect" :haveCollect="questionData.haveCollect" > <view class="page-op"> <view class="btn btn-mode" @click="changeMode"> <text>{{ readMode == "0" ? "背题模式" : "阅读模式" }}</text> <text class="iconfont icon-exchange"></text> </view> <view class="btn show-answer" v-if="readMode === '0'" @click="showAnswerHandler" >查看答案</view > </view> </Footer> </view> </template> <script setup> import uvParse from "@/pages/components/uv-parse/components/uv-parse/uv-parse.vue"; import QuestionItem from "./QuestionItem.vue"; import { LOCAL_STORAGE_KEY } from "@/utils/Constants.js"; import { onLoad } from "@dcloudio/uni-app"; import { ref, reactive, getCurrentInstance, nextTick } from "vue"; 总结这段代码代码功能流
最新发布
03-17
<think>好的,我需要分析用户提供的Vue代码片段中的各个组件和逻辑处理函数之间的交互关系与功能实现。首先,我会逐一查看每个组件的功能,然后理解它们如何通过Vue的机制进行通信和协作。 首先看Navbar组件。它可能负责顶部导航栏,包含模式切换按钮,比如“背题模式”和“答题模式”。这里涉及到changeMode函数,当用户切换模式时,这个函数会被触发,可能通过Vuex或props来更新应用的状态,影响其他组件的显示方式,比如题目是否显示答案。 接下来是ShowTips组件,可能用于显示提示信息,比如收藏状态的变化。当用户点击收藏按钮时,updateCollect函数会被调用,这里可能通过inject获取父组件提供的方法,然后调用collectHandle。成功后,ShowTips会显示提示,比如“收藏成功”或“已取消收藏”。这说明ShowTips依赖于父组件或通过provide/inject机制获取显示内容。 SlidePage组件处理滑动翻页,监听左右滑动事件来触发loadDataNext函数,加载下一页或上一页的数据。loadDataNext可能通过API获取数据,并更新当前显示的题目数据。这里涉及到异步操作,可能需要处理加载状态和错误提示。 Footer组件显示在底部,包含收藏按钮和提交按钮。收藏按钮绑定updateCollect方法,提交按钮可能触发提交动作。Footer的显示可能通过全局状态管理,或者根据路由动态显示,如引用[3]提到的使用v-on:footer事件来控制显示。 逻辑处理函数方面,loadDataNext负责数据加载,可能使用axios或fetch进行API调用,更新数据后触发重新渲染。updateCollect处理收藏逻辑,可能通过inject获取收藏方法,如引用[1]中的inject用法。changeMode切换应用模式,影响其他组件的行为,比如是否直接显示答案。showAnswerHandler可能在答题模式下显示答案,与SlidePage中的题目展示联动。 组件间通信主要通过Vue的provide/inject机制,比如父组件提供collectHandle方法,子组件通过inject获取并调用。引用[1]提到inject的使用,说明跨组件通讯的可能。事件监听如@swipe-left和@swipe-right处理滑动,触发数据加载。引用[2]中的ref绑定可能用于获取组件实例,但需注意正确绑定以避免失败。 总结各组件的关系:Navbar控制模式切换,影响SlidePage的显示方式;SlidePage通过滑动加载数据,触发loadDataNext;Footer中的按钮触发收藏和提交,与ShowTips联动显示提示;所有组件通过状态管理和事件传递协同工作,实现完整的题目练习功能。</think>### Vue组件功能流程分析 #### 1. 组件结构及核心功能 - **Navbar**:顶部导航栏组件,包含模式切换按钮 - 通过`changeMode`实现背题/答题模式切换 - 模式状态通过Vuex或props传递到子组件[^1] - **ShowTips**:全局提示组件 - 显示收藏状态变化提示(如"收藏成功") - 通过`v-if="showTips"`控制显隐,与`updateCollect`联动 - **SlidePage**:滑动翻页组件 - 监听`@swipe-left`/`@swipe-right`事件触发`loadDataNext` - 根据`currentMode`显示答案或题目(`showAnswerHandler`) - **Footer**:底部操作栏 - 包含收藏按钮和提交按钮 - 使用`inject('isShowFooter')`控制显示逻辑[^3] #### 2. 关键交互流程 $$ \begin{cases} \text{模式切换} \xrightarrow{changeMode} \text{更新currentMode} \xrightarrow{watch} \text{SlidePage重渲染}\\ \text{滑动操作} \xrightarrow{loadDataNext} \text{API请求} \xrightarrow{then/catch} \text{数据更新/错误提示}\\ \text{收藏操作} \xrightarrow{updateCollect} \xrightarrow{inject('collectHandle')} \text{ShowTips提示} \end{cases} $$ #### 3. 核心函数实现 ```javascript // 数据加载示例 const loadDataNext = (direction) => { loading.value = true API.get(`/next?dir=${direction}`) .then(res => { questionData.value = res.data currentIndex.value = direction === 'next' ? 0 : res.data.length-1 }) .finally(() => loading.value = false) } // 模式切换示例 const changeMode = () => { currentMode.value = currentMode.value === 'exam' ? 'study' : 'exam' localStorage.setItem('mode', currentMode.value) // 状态持久化 } ``` #### 4. 组件通信机制 1. **provide/inject**:父组件提供`collectHandle`方法,`updateCollect`通过`inject`调用 2. **事件传递**:`@swipe-left`事件绑定到`loadDataNext('prev')` 3. **ref绑定**:`<SlidePage ref="slider">`用于直接调用组件方法[^2] 4. **状态管理**:通过Vuex或组合式API共享`currentMode`状态
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值