写文章不容易,点个赞呗兄弟 <br> <br> 专一 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工做原理,源码版助于了解内部详情,让咱们一块儿学习吧 研究基于 Vue版本 【2.5.17】html
若是你以为排版难看,请点击 下面连接 或者 拉到 下面关注公众号也能够吧express
【Vue原理】Compile - 源码版 之 generate 节点数据拼接 api
上一篇咱们讲了不一样节点的拼接,这一篇须要详细记录的是 节点数据的拼接数组
节点数据,包括有 props,attrs,事件等dom
上一篇咱们在 genElement 中看到过,每一个节点都须要去拼接节点数据,使用的就是下面源码中的 genData$2 这个方法函数
function genElement() { .....处理其余类型的节点 var data = genData$2(el, state); var children = genChildren(el, state); code = `_c('${el.tag}', $ { data ? ("," + data) : '' }, $ { children ? ("," + children) : '' })` }
这个函数的源码有点长,可是不用怕,都是处理各类属性的判断,因此内容大约一致,不过里面涉及到具体的方法,会具体看学习
来吧,先过一遍把spa
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 }
首先这个方法,最终返回的是一个对象的序列化字符串,好比这样3d
" { a:b , c:d } "
因此头尾都会 加上大括号,而后属性拼接xx:yy 的形式code
下面咱们就来一个个看对于不一样属性的处理
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} }]`
每个指令,都会解析成一个对象字符串,而后拼接在字符串数组里面
那么下面就来详细记录几个可能疑惑的点
在上面文章中的 CodegenState 中,咱们有写过这个
state.directives 是一个数组,包含了 Vue内部指令的处理函数,以下
v-on,v-bind,v-cloak,v-model ,v-text,v-html
一个标志位,表示是否须要把指令的数据解析成一个 对象字符串,像这样
`{ 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>) } })`
一个标志位,表示是否须要把 return 指令字符串
genDirectives 处理的是一个指令数组,当数组为空的时候,并不会有返回值
那么 render 字符串就不会 存在 directive 这一段字符串
若是指令不为空,那么 hasRunTime 设为 true,须要返回字符串
而且在 字符串尾部加上 ] , 这样字符串数组就完整了
这里的解析组件,解析的是带有 is 属性的绑定组件
很简单,就是拼接上一个 tag 的属性就ok 了
看例子
原有的标签名,被拼接在 tag 后面
` _c("test",{tag:"div"}) `
上篇文章也说过,state.dataGenFns 是一个数组
存放的是两个函数,一个是解析 class ,一个是解析 style 的
这里放下其中的源码,很是的简单
function genData(el) { var data = ''; if (el.staticClass) { data += "staticClass:" + el.staticClass + ","; } if (el.classBinding) { data += "class:" + el.classBinding + ","; } return data }
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 中,拼接的结果会有两种
为何会拼接到不一样的地方?
由于看的是你属性 放的位置
若是你的属性位置是 标签上,那么就会拼接到 attr 中
若是你的属性位置是在 dom 上,那么就被拼接到 domProps 中
举个例子
好比下面的模板,bbb 就是放在 标签上,aaa 就是放在 DOM 上
拼接的结果就是
` _c('div',{ attrs:{"bbb":"bbb"}, domProps:{"aaa":11} }) `
页面标签看不到 aaa
能够在 dom 属性中找到 aaa
事件的拼接,内容不少,打算放在另外一篇文章详细记录
事件拼接还分为两种,原生事件和 自定义事件,只是拼接为不一样字符串而已,可是处理方法同样
方法中涉及到各类 修饰符,哈哈,想知道到底为何能写出这么方便的 api 呢哈哈
绑定按键,阻止默认事件,直接这么写就好了
@keyup.enter.prevent="xxx"
欢迎观看下篇文章
就是直接拼接上 slot 这个属性
` _c('test',[_c('span',{ attrs:{"slot":"name"}, slot:"name" })] ) `
若是组件有slot,没有 slot 这个属性,那么就不会拼接上slot,后面会直接给个默认名字 “default”
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 自带的一个 模板节点,是不存在的
没错,这里的 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')]) ]) `
鉴于本人能力有限,不免会有疏漏错误的地方,请你们多多包涵,若是有任何描述不当的地方,欢迎后台联系本人,领取红包