寫文章不容易,點個讚唄兄弟 <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')]) ]) `
鑑於本人能力有限,不免會有疏漏錯誤的地方,請你們多多包涵,若是有任何描述不當的地方,歡迎後臺聯繫本人,領取紅包