写文章不容易,点个赞呗兄弟
专一 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工做原理,源码版助于了解内部详情,让咱们一块儿学习吧 研究基于 Vue版本 【2.5.17】数组
若是你以为排版难看,请点击 下面连接 或者 拉到 下面关注公众号也能够吧bash
【Vue原理】Compile - 源码版 之 属性解析 dom
哈哈哈,今天终于到了属性解析的部分了,以前已经讲过了 parse 流程,标签解析,最后就只剩下 属性解析了 (´・ᴗ・`)ide
若是你对 compile 不感兴趣的就先不看把,毕竟不会立刻起到什么做用~~ヾ(●´∀`●)函数
若是大家没看过前面两篇文章的,十分建议看一下~学习
Compile 之 Parse 主要流程 Compile 之 标签解析 ui
若是看了,大家应该知道《属性解析》在哪部分中,没错,在处理 头标签的 部分 parse-start 中this
那么咱们就来到 parse - start 这个函数中!spa
看到下面的源码中,带有 process 的函数都是用于处理 属性的3d
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-if
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else {
currentParent.children.push(element);
element.parent = currentParent;
}
}
}
复制代码
看完了吧,上面处理属性的函数大概有几个
没啥难的,就是内容多了点
一、processFor,解析 v-for
二、processIf,解析 v-if
三、processSlot,解析 slot
四、processAttrs,解析其余属性
五、transforms,解析样式属性
复制代码
而且只有 element.processed 为 false 的时候,才会进行解析
由于 element.processed 表示属性已经解析完毕,一开始 element.processed 的值是 undefined
下面就会逐个说明上面的方法
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 这个函数的 开头,咱们看到有一个 transfroms 的东西
transforms 是一个数组,存放两个函数,一个是处理 动静态的 class,一个处理 动静态的 style
两种处理都很简单的,咱们来简单看看处理结果就行了
function transformNode(el, options) {
var staticClass = getAndRemoveAttr(el, 'class');
if (staticClass) {
el.staticClass = JSON.stringify(staticClass);
}
// :class="b" 直接返回 b
var classBinding = getBindingAttr(el, 'class', false);
if (classBinding) {
el.classBinding = classBinding;
}
}
复制代码
{
classBinding: "b"
staticClass: ""a""
tag: "span"
type: 1
}
复制代码
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
}
复制代码
在 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,
}
复制代码
在 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 的,不然都会报错,错误就不讨论了
在 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: 1
children:[{
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
}
复制代码
下面内容不少,可是不难
这一块内容不少,可是总的来讲没有难度,就是看得烦了一些,而后把源码放到了最后,打算先写解析
这里集中处理了剩下的其余类型的属性,大体分了两种状况
好比 带有 "v-" , ":" , " @" 三种符号的属性名,这三种每种都会分开处理
而在这三种属性开始处理前,会把属性名带有的 modifiers 给提取出来
好比带有 modifiers 的指令
v-bind.a.b.c = "xxxx"
复制代码
通过处理,会提取出 modifiers 对象,以下
{a: true, b: true, c: true}
复制代码
以供指令使用
以后就开始处理三种类型属性
咱们都知道 " : " 等于 "v-bind" ,全部当匹配到这种属性名的时候,会进入这里的处理
大体看一遍以后,能够看到,通过这部分的处理
属性会存放进 el.props 或者 el.attrs
那么问题来了?
怎么判断属性放入 el.props 仍是 el.attrs 呢?
有两种条件
一、modifiers.prop
当你给指令添加了 .prop 的时候,好比
<div :sex.prop="myName"></div>
复制代码
那么 sex 这个属性,就会被存放到 el.props
二、表单
你看到这一句代码
!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 - 源码版 之 绑定组件自定义事件
当匹配到 @ 或者 v-on 的时候,属于添加事件,这里没有太多处理
addHandler 就是把全部事件保存到 el.events
剩下 带有 v- 的属性,都会放到这里处理
匹配参数的,源码中注释也说清楚了,这里不解释了
而后通通保存到 el.directives 中
没啥说的,普通属性,直接存放进 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 dynamic
el.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-bind
name = name.replace(modifierRE, '');
}
// 收集动态属性,v-bind,多是绑定的属性,多是传入子组件的props
// bindRE = /^:|^v-bind:/
if (bindRE.test(name)) {
// 抽取出纯名字,好比 name= v-bind
// 替换以后,name = bind
name = 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
});
}
}
}
复制代码
鉴于本人能力有限,不免会有疏漏错误的地方,请你们多多包涵,若是有任何描述不当的地方,欢迎后台联系本人,有重谢