element 往node里面增加属性值_【Vue原理】Compile - 源码版 之 Parse 属性解析
写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧
【Vue原理】Compile - 源码版 之 属性解析
哈哈哈,今天终于到了属性解析的部分了,之前已经讲过了 parse 流程,标签解析,最后就只剩下 属性解析了 (´・ᴗ・`)
如果你对 compile 不感兴趣的就先不看把,毕竟不会马上起到什么作用~~ヾ(●´∀`●)
如果你们没看过前面两篇文章的,十分建议看一下~
Compile 之 Parse 主要流程 Compile 之 标签解析
如果看了,你们应该知道《属性解析》在哪部分中,没错,在处理 头标签的 部分 parse-start 中
那么我们就来到 parse - start 这个函数中!
看到下面的源码中,带有 process 的函数都是用于处理 属性的
function parse(template){parseHTML(template,{ start:(...抽出放下面)})
}function start(tag, attrs, unary) { // 创建 AST 节点var element = createASTElement(tag, attrs, currentParent); // 节点需要解析,并没有还没有处理if (!element.processed) {processFor(element);processIf(element);processSlot(element); for (var i = 0; i < transforms.length; i++) {element = transforms[i](element, options) || element;}processAttrs(element);}.... 省略部分不重要代码 // 父节点就是上一个节点,直接放入 上一个节点的 children 数组中if (currentParent) { // 说明前面节点有 v-ifif (element.elseif || element.else) {processIfConditions(element, currentParent);} else {currentParent.children.push(element);element.parent = currentParent;}}
}
看完了吧,上面处理属性的函数大概有几个
没啥难的,就是内容多了点
1、processFor,解析 v-for2、processIf,解析 v-if3、processSlot,解析 slot4、processAttrs,解析其他属性5、transforms,解析样式属性
并且只有 element.processed 为 false 的时候,才会进行解析
因为 element.processed 表示属性已经解析完毕,一开始 element.processed 的值是 undefined
下面就会逐个说明上面的方法
先明确下 element 是什么?
parse 流程中说过了,element 是 通过解析得到的 tag 信息,生成的 ast
下面会逐个分析下上面的四个函数,并会附上相应的 element 例子作为参考
其实还有很多其他处理函数,为了维持文章的长度,所以我去掉了
开篇之前,大家需要先了解 getAndRemoveAttr 这个函数,下面很多地方都会使用到
作用就是从 el.attrList 中查找某个属性,返回返回属性值
function getAndRemoveAttr(el, name, removeFromMap) { var val =el.attrsMap[name]; if (removeFromMap) { delete el.attrsMap[name];} return val}
parse-start 中的 tramsforms
在parse -start 这个函数的 开头,我们看到有一个 transfroms 的东西
transforms 是一个数组,存放两个函数,一个是处理 动静态的 class,一个处理 动静态的 style
两种处理都很简单的,我们来简单看看处理结果就好了
处理 class
function transformNode(el, options) { var staticClass = getAndRemoveAttr(el, 'class'); if (staticClass) {el.staticClass = JSON.stringify(staticClass);} // :class="b" 直接返回 bvar classBinding = getBindingAttr(el, 'class', false); if (classBinding) {el.classBinding = classBinding;}
}
{ classBinding: "b"staticClass: ""a""tag: "span"type: 1}
处理 style
function transformNode$1(el, options) { var staticStyle = getAndRemoveAttr(el, 'style'); if (staticStyle) { // 比如绑定 style="height:0;width:0"// parseStyleText 解析得到对象 { height:0,width:0 }el.staticStyle = JSON.stringify(parseStyleText(staticStyle));} // :style="{height:a}" 解析得 {height:a}var styleBinding = getBindingAttr(el, 'style', false); if (styleBinding) {el.styleBinding = styleBinding;}
}
{ staticStyle: "{"width":"0"}"styleBinding: "{height:a}"tag: "span"type: 1}
解析 v-for
在 parse - start 这个函数中,看到了 processFor,没错,就是解析 v-for 指令的!
function processFor(el) { var exp = getAndRemoveAttr(el, 'v-for') if (exp) { // 比如指令是 v-for="(item,index) in arr"// res = {for: "arr", alias: "item", iterator1: "index"}var res = parseFor(exp); if (res) { // 把 res 和 el 属性合并起来extend(el, res);} }
}
没有什么难度,直接看模板 和最终结果好了
<div v-for="(item,index) in arr"></div>
{ alias: "item", for: "arr", iterator1: "index", tag: "div", type: 1,}
解析 v-if
在 parse - start 这个函数中,看到了 processFor,没错,就是解析 v-if 指令的!
function processIf(el) { var exp = getAndRemoveAttr(el, 'v-if'); if (exp) {el.if = exp;(el.ifConditions || el.ifConditions=[]).push({ exp: exp, block: el})} else { if (getAndRemoveAttr(el, 'v-else') != null) {el.else = true;} var elseif = getAndRemoveAttr(el, 'v-else-if'); if (elseif) {el.elseif = elseif;}}
}
处理 v-if 上是这样的,需要把 v-if 的 表达式 和 节点都保存起来
而 v-else ,只需要设置 el.else 为 true,v-else-if 同样需要保存 表达式
在这里 v-else 和 v-else-if 并没有做太多处理,而是在最前面的 parse-start 中有处理
if (element.elseif || element.else) {processIfConditions(element, currentParent);
}
当经过 processIf 之后,该属性存在 elseif 或 else
那么会调用一个方法,如下
function processIfConditions(el, parent) { var prev = findPrevElement(parent.children); if (prev && prev.if) { (prev.ifConditions ||prev.ifConditions=[]).push({ exp: el.elseif, block: el})}
}
这个方法主要是把 带有 v-else-if 和 v-else 的节点挂靠在 带有 v-if 的节点上
先来看挂靠后的结果
<div><p></p><div v-if="a"></div><strong v-else-if="b"></strong><span v-else></span></div>
{ tag: "header", type: 1, children:[{ tag: "header", type: 1, if: "a", ifCondition:[{exp: "a", block: {header的ast 节点}}{exp: "b", block: {strong的ast 节点}}{exp: undefined, block: {span的ast节点}}]},{ tag: "p"type: 1}]
}
我们可以看到,原来写的两个子节点,strong 和 span 都不在 div 的children 中
而是跑到了 header 的 ifCondition 里面
现在看看 processIfConditions , 这个方法是只会处理 带有 v-else-if 和 v-else 的节点的
并且需要找到 v-if 的节点挂靠,怎么找的呢?你可以看到一个方法
function findPrevElement(children) { var i = children.length; while (i--) { if (children[i].type === 1) { return children[i]} else {children.pop();}}
}
从同级子节点中结尾开始找,当type ==1 的时候,这个节点就是带有 v-if 的节点
那么 v-else 那两个就可以直接挂靠在上面了
你会问,为什么从结尾不是返回 span 节点,为什么 type ==1 就是带有 v-if?
首先,你并不能从正常解析完的角度去分析,要从标签逐个解析的角度去分析
比如现在已经解析完了 v-if 的节点,并且添加进了 父节点的 children
然后解析下一个节点,比如这个节点是带有 v-else-if 的节点,此时,再去 parent.children 找最后一个节点(也就是刚刚添加进去的 v-if 节点)
肯定返回的是 v-if 的节点,自然能正确挂靠了
v-else 同理
如果你说 v-if 和 v-else-if 隔了一个其他节点,那 v-else-if 就无法挂靠在 v-if 了呢
那你肯定是刁民,v-else-if 必须跟着 v-if 的,否则都会报错,错误就不讨论了
解析 slot
在 parse - start 这个函数中,看到了 processSlot,没错,就是解析 slot 相关
function processSlot(el) { if (el.tag === 'slot') {el.slotName = el.attrsMap.name} else { var slotScope = getAndRemoveAttr(el, 'slot-scope')el.slotScope = slotScope; // slot 的名字var slotTarget = el.attrsMap.slot if (slotTarget) {el.slotTarget = slotTarget === '""' ? '"default"': slotTarget;}}
}
这个好像也没什么好讲的,就简单记录一下 解析的结果好了
子组件模板
<span><slot name=" header":a="num" :b="num"></slot>
</span>
解析成
{ tag: "span"type: 1children:[{ attrsMap: {name: " header", :a: "num", :b: "num"}slotName: "" header""tag: "slot"type: 1}]
}
父组件模板
<div><child ><p slot="header" slot-scope="c"> {{ c }}</p></child></div>
解析成
{ children: [{ tag: "child", type: 1, children: [{ slotScope: "c", slotTarget: ""header "", tag: "p", type: 1}]}], tag: "div", type: 1}
下面内容很多,但是不难
解析其他属性
这一块内容很多,但是总的来说没有难度,就是看得烦了一些,然后把源码放到了最后,打算先写解析
这里集中处理了剩下的其他类型的属性,大致分了两种情况
1Vue 自带属性
比如 带有 "v-" , ":" , " @" 三种符号的属性名,这三种每种都会分开处理
而在这三种属性开始处理前,会把属性名带有的 modifiers 给提取出来
比如带有 modifiers 的指令
v-bind.a.b.c = "xxxx"
经过处理,会提取出 modifiers 对象,如下
{a: true, b: true, c: true}
以供指令使用
之后就开始处理三种类型属性
1 " : "
我们都知道 " : " 等于 "v-bind" ,所有当匹配到这种属性名的时候,会进入这里的处理
大致看一遍之后,可以看到,经过这部分的处理
属性会存放进 el.props 或者 el.attrs
那么问题来了?
怎么判断属性放入 el.props 还是 el.attrs 呢?
有两种条件
1、modifiers.prop
当你给指令添加了 .prop 的时候,比如
<div :sex.prop="myName"></div>
那么 sex 这个属性,就会被存放到 el.props
2、表单
你看到这一句代码
!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
第一,不能是组件
第二,是表单元素,且是表单重要属性
来看看 platformMustUseProp 吧,很容易
当元素是 input,textarea,option,select,progress
属性是 selected ,checked ,value 等之类的话
都要存放到 el.props 中
function a(tag, type, attr) { return ((attr === 'value' && 'input,textarea,option,select,progress'.indexOf(tag)>-1) && type !== 'button' || (attr === 'selected' && tag === 'option') || (attr === 'checked' && tag === 'input') || (attr === 'muted' && tag === 'video'))
};
或许你会问
el.props 和 el.attrs 有什么区别呢?
props 是直接添加到 dom 属性上的,而不会显示在标签上
attrs 则是用于显示到到 标签属性上的
还有一个问题
添加进 el.props 的属性,为什么要转换成驼峰命名?
你看到的,所有属性名,都会通过一个 camelize 的方法,为什么呢?
因为 DOM 的属性都是驼峰命名的,不存在横杆的命名
所以要把 a-b 的命名都转成 aB,随便截了一张图
然而 innerHTML 比较特殊,驼峰都不行,所以做了特殊处理,你也看到的
驼峰的方法应该挺有用的,放上来吧
var camelize = function(str) { return str.replace(/-(w)/g, function(_, c) { return c ? c.toUpperCase() : ''; })
})
modifiers.sync
之后,你应该还发现了一块宝藏,没错就是 sync
相信你应该用过吧,用于父子通信的,子组件想修改父组件传入的 prop
通过事件的方式,间接修改 父组件的数据,从而更新 props
为了避免大家不记得了,在这里贴一个使用例子
父组件 给 子组件 传入 name ,加入 sync 可以双向修改<div> <child-test :name.sync="xxx"></child-test></div>子组件想修改 父组件传入的 name,直接触发事件并传入参数就可以了this.$emit("update:name", 222)
于是现在我们来看他在属性解析时是怎么实现的
addHandler(el, "update:" + camelize(name),genAssignmentCode(value, "$event")
);
看看这段代码做了什么
首先
camelize(name)
把名字变成驼峰写法,比如 get-name,转换成 getName
然后下面这段代码 执行
genAssignmentCode(value, "$event")
解析返回 "value = $event"
然后 addHandler 就是把 事件名和事件回调保存到 el.events 中,如下
保存的 events 后面会被继续解析,value 会被包一层 function
相当于给子组件监听事件
@update:name ="function($event){ xxx = $event }"
$event 就是子组件触发事件时 传入的值
xxx 是 父组件的数据,赋值之后,就相当于子组件修改父组件数据了
要是想了解 event 的内部原理,可以看 Event - 源码版 之 绑定组件自定义事件
2 " @ "
当匹配到 @ 或者 v-on 的时候,属于添加事件,这里没有太多处理
addHandler 就是把所有事件保存到 el.events
3 " v- "
剩下 带有 v- 的属性,都会放到这里处理
匹配参数的,源码中注释也说清楚了,这里不解释了
然后统统保存到 el.directives 中
2普通属性
没啥说的,普通属性,直接存放进 el.attrs
下面就是处理其他属性的源码,你别看很长,其实很简单的!
var onRE = /^@|^v-on:/;var dirRE = /^v-|^@|^:/;var bindRE = /^:|^v-bind:/;var modifierRE = /.[^.]+/g;var argRE = /:(.*)$/;function processAttrs(el) { var list = el.attrsList; var i, l, name, rawName, value, modifiers, isProp; for (i = 0, l = list.length; i < l; i++) {name = rawName = list[i].name;value = list[i].value; // 判断属性是否带有 'v-' , '@' , ':'if (dirRE.test(name)) { // mark element as dynamicel.hasBindings = true; // 比如 v-bind.a.b.c = "xxzxxxx"// 那么 modifiers = {a: true, b: true, c: true}modifiers = parseModifiers(name); // 抽取出纯名字if (modifiers) { // name = "v-bind.a.b.c = "xxzxxxx" "// 那么 name= v-bindname = name.replace(modifierRE, '');} // 收集动态属性,v-bind,可能是绑定的属性,可能是传入子组件的props// bindRE = /^:|^v-bind:/if (bindRE.test(name)) { // 抽取出纯名字,比如 name= v-bind// 替换之后,name = bindname = name.replace(bindRE, '');isProp = false; if (modifiers) { // 直接添加到 dom 的属性上if (modifiers.prop) {isProp = true; // 变成驼峰命名name = camelize(name); if (name === 'innerHtml') name = 'innerHTML'; } // 子组件同步修改if (modifiers.sync) {addHandler(el, // 得到驼峰命名 "update:" + camelize(name), // 得到 "value= $event"genAssignmentCode(value, "$event"));}} // el.props 的作用上面有说,这里有部分是 表单的必要属性都要保存在 el.props 中if (isProp ||// platformMustUseProp 判断这个属性是不是要放在 el.props 中// 比如表单元素 input 等,属性是value selected ,checked 等// 比如 tag=input,name=value,那么value 属性要房子啊 el.props 中(!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))) {(el.props || (el.props = [])).push({ name, value});} // 其他属性放在 el.attrs 中else {(el.attrs || (el.attrs = [])).push({ name, value});}} // 收集事件,v-on , onRE = /^@|^v-on:/else if (onRE.test(name)) { // 把 v-on 或者 @ 去掉,拿到真正的 指令名字// 比如 name ="@click" , 替换后 name = "click"name = name.replace(onRE, '');addHandler(el, name, value, modifiers, false);} // 收集其他指令,比如 "v-once",else { // 把v- 去掉,拿到真正的 指令名字name = name.replace(dirRE, ''); // name = "bind:key" , argMatch = [":a", "a"]var argMatch = name.match(argRE); var arg = argMatch && argMatch[1]; if (arg) { // 比如 name = "bind:key" ,去掉 :key// 然后 name = "bind"name = name.slice(0, -(arg.length + 1));}(el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers});}} else {(el.attrs || (el.attrs = [])).push({ name, value});}}
}
最后
鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢
element 往node里面增加属性值_【Vue原理】Compile - 源码版 之 Parse 属性解析相关推荐
- 增加数组下标_数组以及ArrayList源码解析
点击上方"码之初"关注,···选择"设为星标" 与精品技术文章不期而遇 前言 前一篇我们对数据结构有了个整体的概念上的了解,没看过的小伙伴们可以看我的上篇文章: ...
- js修改id的值_如何修改pytesthtml源码来优化接口自动化测试报告
你这么优秀,一定只想把"柠檬班"置顶 ▲ 前言 大家好,我是柠檬班Python6期的学员小翟. 以前常用unittest做接口自动化测试,后来想获取单个接口的响应时间,便采用pyt ...
- java如何获取数组中的属性值_【java】查找对象数组中某属性的最大值, 然后返回该项的其他属性值...
有一个数组,里面装的都是对象. var array=[ { "id": 52354541, "name": "比率", "valu ...
- c++ gdb 绑定源码_【Vue原理】VNode 源码版
↑点击上方 "神仙朱" 一起研究Vue源码吧 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版 ...
- v-model双向绑定原理_【Vue原理】VModel 白话版
↑点击上方 "神仙朱" 一起研究Vue源码吧 专注 Vue 源码分享,为了方便大家理解,分为了白话版和 源码版,白话版让大家可以轻松理解工作原理,源码版让大家更清楚内部操作和 Vu ...
- vue使用computed有参数_【Vue原理】Computed - 源码版
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本2.5.17 今天要记录 computed 的源码,有时候想,理 ...
- 初始化触发点击事件_【Vue原理】Event - 源码版 之 自定义事件
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本[2.5.17] Vue 的自定义事件很简单,就是使用 观察者模 ...
- js 加总数组中某一列_js根据对象数组中某一属性值,合并相同项,并对某一属性累加处理...
js根据对象数组中某一属性值,合并相同项,并对某一属性累加处理 Example: 根据code合并数组,并将sl值累加,如下两种方法: let data = [{ code: 1001, name: ...
- vue源码-对于「计算属性」的理解
vue源码-对于「计算属性」的理解 这是我最近学习vue源码的一个个人总结和理解,所以可能并不适合每一位读者 本文的整体脉络如下,首先尽可能去掉细节,对计算属性源码的大致实现有一个了解,然后举一例子, ...
最新文章
- Python中的__name__和__main__含义详解
- redis演练(5) redis持久化
- Linux 信号可靠性,同步,异步,多线程信号等介绍
- php和mysql web开发 笔记_PHP和MySQL Web开发读书笔记---创建Web数据库
- 帮助别人是一种快乐!
- chrome浏览器遭eFast浏览器恶意软件删除取代
- python后台返回cookie_Django框架设置cookies与获取cookies操作详解
- java演练 猜奇偶小游戏开发 DB游戏必输的设计
- 微信Mac版可以发朋友圈了 还能浏览相册
- Django中的request和response
- 学习笔记:pscc2020基础
- I2C 时序详解,精确到每一个时钟
- 代码审查工具 FindBugs
- 64位操作系统最大虚拟内存16TB
- Windows权限维持方法
- virtual memory exhausted: Cannot allocate memory 解决办法 命令分配交换空间
- 上海宝付揭穿网络招聘小把戏
- php浏览器跟踪调试,Phpstorm怎么在命令行以及浏览器中调试
- 蓝桥杯2017国赛 瓷砖样式 dfs+map
- spss多因素方差分析