共计 6195 个字符,预计需要花费 16 分钟才能阅读完成。
写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue 版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面 关注公众号 也可以吧
【Vue 原理】Compile – 源码版 之 generate 节点数据拼接
上一篇我们讲了不同节点的拼接,这一篇需要详细记录的是 节点数据的拼接
节点数据,包括有 props,attrs,事件等
上一篇我们在 genElement 中看到过,每个节点都需要去拼接节点数据,使用的就是下面源码中的 genData$2 这个方法
function genElement() {
..... 处理其他类型的节点
var data = genData$2(el, state);
var children = genChildren(el, state);
code = `_c('${el.tag}', $ {data ? ("," + data) : ''
}, $ {children ? ("," + children) : ''
})`
}
genData$2
这个函数的源码有点长,但是不用怕,都是处理各种属性的判断,所以内容大约一致,不过里面涉及到具体的方法,会具体看
来吧,先过一遍把
function genData$2(el, state) {
var data = '{';
// 先解析指令
var dirs = genDirectives(el, state);
// 拼接上解析得到的指令字符串
if(dirs) {data += dirs + ',';}
// 带有 is 绑定的组件,直接使用组件则没有
if(el.component) {data += `tag: ${el.tag} , `
}
// 上一篇说过的,dataGenFns 包含处理 style,class 的函数
for(var i = 0; i < state.dataGenFns.length; i++) {data += state.dataGenFns[i](el);
}
// 全部属性
if(el.attrs) {data += ` attrs:{ ${genProps(el.attrs)) } ,`
}
// 原生属性
if(el.props) {data += ` domProps:{ ${genProps(el.props)} }, `
}
// 事件
if(el.events) {data += genHandlers(el.events, false) + ",";
}
// 原生事件
if(el.nativeEvents) {data += genHandlers(el.nativeEvents, true) + ",";
}
// 没有作用域的 slot
if(el.slotTarget && !el.slotScope) {data += ` slot: ${ el.slotTarget} ,`
}
// 作用域 slot
if(el.scopedSlots) {data += genScopedSlots(el.scopedSlots, state) + ",";
}
// 组件使用 v-model
if(el.model) {
data += `model:{value:${el.model.value},
callback:${el.model.callback},
expression:${el.model.expression},
},`
}
data = data.replace(/,$/, '') +'}';
return data
}
首先这个方法,最终返回的是一个对象的序列化字符串,比如这样
"{a:b , c:d}"
所以头尾都会 加上大括号,然后属性拼接 xx:yy 的形式
下面我们就来一个个看对于不同属性的处理
拼接指令
function genDirectives(el, state) {
var dirs = el.directives;
if (!dirs) return
var res = 'directives:[';
var hasRuntime = false;
var i, l, dir, needRuntime;
for (i = 0, l = dirs.length; i < l; i++) {dir = dirs[i];
needRuntime = true;
// 获取到特定的 Vue 指令处理方法
var gen = state.directives[dir.name];
// 如果这个函数存在,证明这个指令是内部指令
if (gen) {needRuntime = gen(el, dir);
}
if (needRuntime) {
hasRuntime = true;
res += `{name: ${dir.name},
rawName: ${dir.rawName}
${
dir.value
? ",value:" + dir.value +", expression:" + dir.value
: ''
}
${dir.arg ? ( ",arg:" + dir.arg) : '' }
${
dir.modifiers
? (",modifiers:" + JSON.stringify(dir.modifiers))
: ''
}
}, `
}
}
if (hasRuntime) {return res.slice(0, -1) + ']'
}
}
首先呢,我们要了解这个方法会返回什么字符串,比如
就会返回这样的字符串
`directives:[{
name:"test",
rawName:"v-test:a.b.c",
value:222,
expression:"arr",
arg:"a",
modifiers:{"b":true,"c":true}
}]`
每一个指令,都会解析成一个对象字符串,然后拼接在字符串数组里面
那么下面就来详细记录几个可能疑惑的点
函数中出现的 state.directives
在上面文章中的 CodegenState 中,我们有写过这个
state.directives 是一个数组,包含了 Vue 内部指令的处理函数,如下
v-on,v-bind,v-cloak,v-model,v-text,v-html
函数中的变量 needRuntime
一个标志位,表示是否需要把指令的数据解析成一个 对象字符串,像这样
`{
name:xxx, rawName:xxx,
value:xxx, expression:xx,
arg:xx, modifiers:xx
}`
也就是说,这个指令是否需要被拼接成 render 字符串中
那么什么指令需要,什么指令不需要呢?
自定义指令,都需要被解析,拼接在 render 字符串中
但是 Vue 的内部指令,有的用,有的不用,所以就搞出一个 needRunTime 来进行判断
Vue 的指令,先要获取到特定的处理方法,赋值给 gen
gen 处理完返回 true,则表示需要 拼接上 render,返回 false 或者不返回,则表示不需要拼接上
比如,v-model 指令的数据就需要拼接上 render,而 v-text,v-html 则不用
看下面的例子
比如上面的模板拼接成下面的字符串,发现 v-html 并没有出现在 directives 那个字符串数组中
`_c('div',{
directives:[{
name:"model",
rawName:"v-model",
value:arr,
expression:"arr"
}],
domProps:{"innerHTML":_s(<span></span>)
}
})`
函数中的变量 hasRuntime
一个标志位,表示是否需要把 return 指令字符串
genDirectives 处理的是一个指令数组,当数组为空的时候,并不会有返回值
那么 render 字符串就不会 存在 directive 这一段字符串
如果指令不为空,那么 hasRunTime 设为 true,需要返回字符串
并且在 字符串尾部加上 ] , 这样字符串数组就完整了
拼接组件
这里的解析组件,解析的是带有 is 属性的绑定组件
很简单,就是拼接上一个 tag 的属性就 ok 了
看例子
原有的标签名,被拼接在 tag 后面
` _c("test",{tag:"div"}) `
拼接样式
上篇文章也说过,state.dataGenFns 是一个数组
存放的是两个函数,一个是解析 class,一个是解析 style 的
这里放下其中的源码,非常的简单
解析 class
function genData(el) {
var data = '';
if (el.staticClass) {data += "staticClass:" + el.staticClass + ",";}
if (el.classBinding) {data += "class:" + el.classBinding + ",";}
return data
}
解析 style
function genData$1(el) {
var data = '';
if (el.staticStyle) {data += "staticStyle:" + el.staticStyle + ",";}
if (el.styleBinding) {data += "style:(" + el.styleBinding + "),";
}
return data
}
实在是太简单的,就是直接拼接上几个属性而已啦
给例子就好了
`_c('div',{
staticClass:"a",
class:name,
staticStyle:{"height":"0"},
style:{width:0}
})
`
拼接属性
属性的拼接只有一个函数,内容也十分简单
function genProps(props) {
var res = '';
for (var i = 0; i < props.length; i++) {var prop = props[i];
res += prop.name + ":" +
prop.value + ",";
}
return res.slice(0, -1)
}
你可以看到,虽然只有一个方法,但是在 genData$2 中,拼接的结果会有两种
拼接到 el.attr
拼接到 el.props
为什么会拼接到不同的地方?
因为看的是你属性 放的位置
如果你的属性位置是 标签上,那么就会拼接到 attr 中
如果你的属性位置是在 dom 上,那么就被拼接到 domProps 中
举个例子
比如下面的模板,bbb 就是放在 标签上,aaa 就是放在 DOM 上
拼接的结果就是
` _c('div',{attrs:{"bbb":"bbb"},
domProps:{"aaa":11}
}) `
页面标签看不到 aaa
可以在 dom 属性中找到 aaa
拼接事件
事件的拼接,内容很多,打算放在另一篇文章详细记录
事件拼接还分为两种,原生事件和 自定义事件,只是拼接为不同字符串而已,但是处理方法一样
方法中涉及到各种 修饰符,哈哈,想知道到底为什么能写出这么方便的 api 呢哈哈
绑定按键,阻止默认事件,直接这么写就行了
@keyup.enter.prevent="xxx"
欢迎观看下篇文章
拼接普通 Slot
就是直接拼接上 slot 这个属性
` _c('test',[_c('span',{attrs:{"slot":"name"},
slot:"name"
})]
) `
如果组件有 slot,没有 slot 这个属性,那么就不会拼接上 slot,后面会直接给个默认名字“default”
拼接作用域 Slot
function genScopedSlots(slots, state) {
return `
scopedSlots:_u([${Object.keys(slots).map(key =>{return genScopedSlot(key, slots[key], state)
})
.join(',')
}])
`
}
function genScopedSlot(key, el, state) {
var fn = `
function(${el.slotScope}){
return ${
el.tag === 'template'
? genChildren(el, state)
: genElement(el, state)
}
}
`
return `{key:${key} , fn: ${fn} }`
}
这个处理作用域 slot 的函数看起来好像有一点复杂,但是其实就是纸老虎
不怕,先看一个实例
拼接成字符串,是这样的
`
_c('div',{
scopedSlots:_u([{
key:"heder",
fn:function(arr){return _c('div')}
}])
})
`
这个函数遍历的是 el.scopeSlots 这个数组,或许你不知道这个数组是什么内容?
同样给个例子,这里有两个 slot
经过 parse 解析之后成一个 ast,是这样的
{
tag:"test",
scopedSlots:[{
slotScope: "arr"
slotTarget: ""a""
tag: "div"
},{
slotScope: "arr"
slotTarget: ""b""
tag: "div"
}]
}
没错,遍历的就是上面对象里面的 scopedSlots 数组,数组中的每一项都是一个单独的 slot
然后会使用 genScopeSlot 去单独处理一下,上面有放出源码
处理完之后,形成一个新的数组,genScopeSlot 也没什么好说的
拼接分类型,需要判断 slot 位置的标签是不是 template
如果是 template,那么他的真实 slot 是 template 的子节点,直接获取他的子节点
如果不是 template,那么本身就是真实的 slot
因为 template 是 Vue 自带的一个 模板节点,是不存在的
拼接组件 VModel
没错,这里的 model,只是属于 组件的 v-model
if (el.model) {
data += `model: {value: $ { el.model.value},
callback: $ {el.model.callback},
expression: $ {el.model.expression},
}, `
}
官网说了这个是怎么用的
一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件
也就是说,起始就是给组件传了一个 value,绑定了一个事件 input
也没有什么好讲的,记录下组件的 v-model 是这么拼接就好了
例子
经过 parse 解析,得到 ast
{
tag: "test",
model:{callback: "function ($$v) {num=$$v}"
expression: ""num""
value: "num"
}
}
拼接成字样变成字符串了
`
_c('test',{
model:{
value:num,
callback:function ($$v) {num=$$v},
expression:"num"
}
})
`
举个栗子
属性拼接呢,我们就讲完了,最后我们来看一个例子吧
下面这个模板,我们把它拼接起来
解析成下面这个 render 字符串,看懂了,你就掌握了 generate 的内容了
以后你就可以去看别人用 Vue 写的打包后的代码了
甚至,你可以手动还原他,如果你闲得很,你可以自己写个方法,传入 render 字符串,自动还原成 template 模板
` _c('div', {
attrs: {"b": "2"},
domProps: {"a": 11}
},[
_c('test', {
scopedSlots: _u([{
key: "a",
fn: function(arr) {return _c('strong')
}
}]),
model: {value: (num),
callback: function($$v) {num = $$v},
expression: "num"
}
},[_c('span')])
]) `
最后
鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,领取红包