资讯专栏INFORMATION COLUMN

【Vue原理】Compile - 源码版 之 generate 节点数据拼接

fizz / 551人阅读

摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之节点数据拼接上一篇我们

</>复制代码

  1. 写文章不容易,点个赞呗兄弟


    专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
    研究基于 Vue版本 【2.5.17

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧

【Vue原理】Compile - 源码版 之 generate 节点数据拼接

上一篇我们讲了不同节点的拼接,这一篇需要详细记录的是 节点数据的拼接

节点数据,包括有 props,attrs,事件等

上一篇我们在 genElement 中看到过,每个节点都需要去拼接节点数据,使用的就是下面源码中的 genData$2 这个方法

</>复制代码

  1. function genElement() {
  2. .....处理其他类型的节点
  3. var data = genData$2(el, state);
  4. var children = genChildren(el, state);
  5. code = `_c("${el.tag}", $ {
  6. data ? ("," + data) : ""
  7. }, $ {
  8. children ? ("," + children) : ""
  9. })`
  10. }
genData$2

这个函数的源码有点长,但是不用怕,都是处理各种属性的判断,所以内容大约一致,不过里面涉及到具体的方法,会具体看

来吧,先过一遍把

</>复制代码

  1. function genData$2(el, state) {
  2. var data = "{";
  3. // 先解析指令
  4. var dirs = genDirectives(el, state);
  5. // 拼接上解析得到的指令字符串
  6. if(dirs) {
  7. data += dirs + ",";
  8. }
  9. // 带有 is 绑定的组件,直接使用组件则没有
  10. if(el.component) {
  11. data += `tag: ${el.tag} , `
  12. }
  13. // 上一篇说过的,dataGenFns 包含处理style,class的函数
  14. for(var i = 0; i < state.dataGenFns.length; i++) {
  15. data += state.dataGenFns[i](el);
  16. }
  17. // 全部属性
  18. if(el.attrs) {
  19. data += ` attrs:{ ${genProps(el.attrs)) } ,`
  20. }
  21. // 原生属性
  22. if(el.props) {
  23. data += ` domProps:{ ${genProps(el.props)} }, `
  24. }
  25. // 事件
  26. if(el.events) {
  27. data += genHandlers(el.events, false) + ",";
  28. }
  29. // 原生事件
  30. if(el.nativeEvents) {
  31. data += genHandlers(el.nativeEvents, true) + ",";
  32. }
  33. // 没有作用域的 slot
  34. if(
  35. el.slotTarget && !el.slotScope
  36. ) {
  37. data += ` slot: ${ el.slotTarget } ,`
  38. }
  39. // 作用域slot
  40. if(el.scopedSlots) {
  41. data += genScopedSlots(el.scopedSlots, state) + ",";
  42. }
  43. // 组件使用 v-model
  44. if(el.model) {
  45. data += `model:{
  46. value:${el.model.value},
  47. callback:${el.model.callback},
  48. expression:${el.model.expression},
  49. },`
  50. }
  51. data = data.replace(/,$/, "") + "}";
  52. return data
  53. }

首先这个方法,最终返回的是一个对象的序列化字符串,比如这样

</>复制代码

  1. " { a:b , c:d } "

所以头尾都会 加上大括号,然后属性拼接xx:yy 的形式

下面我们就来一个个看对于不同属性的处理

拼接指令

</>复制代码

  1. function genDirectives(el, state) {
  2. var dirs = el.directives;
  3. if (!dirs) return
  4. var res = "directives:[";
  5. var hasRuntime = false;
  6. var i, l, dir, needRuntime;
  7. for (i = 0, l = dirs.length; i < l; i++) {
  8. dir = dirs[i];
  9. needRuntime = true;
  10. // 获取到特定的 Vue 指令处理方法
  11. var gen = state.directives[dir.name];
  12. // 如果这个函数存在,证明这个指令是内部指令
  13. if (gen) {
  14. needRuntime = gen(el, dir);
  15. }
  16. if (needRuntime) {
  17. hasRuntime = true;
  18. res += `{
  19. name: ${dir.name},
  20. rawName: ${dir.rawName}
  21. ${
  22. dir.value
  23. ? ",value:" + dir.value +", expression:" + dir.value
  24. : ""
  25. }
  26. ${ dir.arg ? ( ",arg:" + dir.arg ) : "" }
  27. ${
  28. dir.modifiers
  29. ? (",modifiers:" + JSON.stringify(dir.modifiers))
  30. : ""
  31. }
  32. }, `
  33. }
  34. }
  35. if (hasRuntime) {
  36. return res.slice(0, -1) + "]"
  37. }
  38. }

首先呢,我们要了解这个方法会返回什么字符串,比如

就会返回这样的字符串

</>复制代码

  1. `directives:[{
  2. name:"test",
  3. rawName:"v-test:a.b.c",
  4. value:222,
  5. expression:"arr",
  6. arg:"a",
  7. modifiers:{"b":true,"c":true}
  8. }]`

每一个指令,都会解析成一个对象字符串,然后拼接在字符串数组里面

那么下面就来详细记录几个可能疑惑的点

函数中出现的 state.directives

在上面文章中的 CodegenState 中,我们有写过这个

state.directives 是一个数组,包含了 Vue内部指令的处理函数,如下

v-on,v-bind,v-cloak,v-model ,v-text,v-html

函数中的变量 needRuntime

一个标志位,表示是否需要把指令的数据解析成一个 对象字符串,像这样

</>复制代码

  1. `{
  2. name:xxx, rawName:xxx,
  3. value:xxx, expression:xx,
  4. arg:xx, modifiers:xx
  5. }`

也就是说,这个指令是否需要被拼接成 render 字符串中

那么什么指令需要,什么指令不需要呢?

自定义指令,都需要被解析,拼接在 render 字符串中

但是 Vue 的内部指令,有的用,有的不用,所以就搞出一个 needRunTime 来进行判断

Vue 的指令,先要获取到特定的处理方法,赋值给 gen

gen 处理完返回 true,则表示需要 拼接上render,返回 false 或者不返回,则表示不需要拼接上

比如,v-model 指令的数据就需要拼接上 render,而 v-text,v-html 则不用

看下面的例子

比如上面的模板拼接成下面的字符串,发现 v-html 并没有出现在 directives 那个字符串数组中

</>复制代码

  1. `_c("div",{
  2. directives:[{
  3. name:"model",
  4. rawName:"v-model",
  5. value:arr,
  6. expression:"arr"
  7. }],
  8. domProps:{
  9. "innerHTML":_s()
  10. }
  11. })`
函数中的变量 hasRuntime

一个标志位,表示是否需要把 return 指令字符串

genDirectives 处理的是一个指令数组,当数组为空的时候,并不会有返回值

那么 render 字符串就不会 存在 directive 这一段字符串

如果指令不为空,那么 hasRunTime 设为 true,需要返回字符串

并且在 字符串尾部加上 ] , 这样字符串数组就完整了

拼接组件

这里的解析组件,解析的是带有 is 属性的绑定组件

很简单,就是拼接上一个 tag 的属性就ok 了

看例子

原有的标签名,被拼接在 tag 后面

</>复制代码

  1. ` _c("test",{tag:"div"}) `
拼接样式

上篇文章也说过,state.dataGenFns 是一个数组

存放的是两个函数,一个是解析 class ,一个是解析 style 的

这里放下其中的源码,非常的简单

解析 class

</>复制代码

  1. function genData(el) {
  2. var data = "";
  3. if (el.staticClass) {
  4. data += "staticClass:" + el.staticClass + ",";
  5. }
  6. if (el.classBinding) {
  7. data += "class:" + el.classBinding + ",";
  8. }
  9. return data
  10. }
解析style

</>复制代码

  1. function genData$1(el) {
  2. var data = "";
  3. if (el.staticStyle) {
  4. data += "staticStyle:" + el.staticStyle + ",";
  5. }
  6. if (el.styleBinding) {
  7. data += "style:(" + el.styleBinding + "),";
  8. }
  9. return data
  10. }

实在是太简单的,就是直接拼接上几个属性而已啦

给例子就好了

</>复制代码

  1. `_c("div",{
  2. staticClass:"a",
  3. class:name,
  4. staticStyle:{"height":"0"},
  5. style:{width:0}
  6. })
  7. `
拼接属性

属性的拼接只有一个函数,内容也十分简单

</>复制代码

  1. function genProps(props) {
  2. var res = "";
  3. for (var i = 0; i < props.length; i++) {
  4. var prop = props[i];
  5. res += prop.name + ":" +
  6. prop.value + ",";
  7. }
  8. return res.slice(0, -1)
  9. }

你可以看到,虽然只有一个方法,但是在 genData$2 中,拼接的结果会有两种

拼接到 el.attr

拼接到 el.props

为什么会拼接到不同的地方?

因为看的是你属性 放的位置

如果你的属性位置是 标签上,那么就会拼接到 attr 中

如果你的属性位置是在 dom 上,那么就被拼接到 domProps 中

举个例子

比如下面的模板,bbb 就是放在 标签上,aaa 就是放在 DOM 上

拼接的结果就是

</>复制代码

  1. ` _c("div",{
  2. attrs:{"bbb":"bbb"},
  3. domProps:{"aaa":11}
  4. }) `

页面标签看不到 aaa

可以在 dom 属性中找到 aaa

拼接事件

事件的拼接,内容很多,打算放在另一篇文章详细记录

事件拼接还分为两种,原生事件和 自定义事件,只是拼接为不同字符串而已,但是处理方法一样

方法中涉及到各种 修饰符,哈哈,想知道到底为什么能写出这么方便的 api 呢哈哈

绑定按键,阻止默认事件,直接这么写就行了

</>复制代码

  1. @keyup.enter.prevent="xxx"

欢迎观看下篇文章

拼接普通Slot

就是直接拼接上 slot 这个属性

</>复制代码

  1. ` _c("test",[_c("span",{
  2. attrs:{"slot":"name"},
  3. slot:"name"
  4. })]
  5. ) `

如果组件有slot,没有 slot 这个属性,那么就不会拼接上slot,后面会直接给个默认名字 “default”

拼接作用域Slot

</>复制代码

  1. function genScopedSlots(slots, state) {
  2. return `
  3. scopedSlots:_u([${
  4. Object.keys(slots).map(key =>{
  5. return genScopedSlot(key, slots[key], state)
  6. })
  7. .join(",")
  8. }])
  9. `
  10. }
  11. function genScopedSlot(key, el, state) {
  12. var fn = `
  13. function(${el.slotScope}){
  14. return ${
  15. el.tag === "template"
  16. ? genChildren(el, state)
  17. : genElement(el, state)
  18. }
  19. }
  20. `
  21. return `{ key:${key} , fn: ${fn} }`
  22. }

这个处理作用域 slot 的函数看起来好像有一点复杂,但是其实就是纸老虎

不怕,先看一个实例

拼接成字符串,是这样的

</>复制代码

  1. `
  2. _c("div",{
  3. scopedSlots:_u([{
  4. key:"heder",
  5. fn:function(arr){return _c("div")}
  6. }])
  7. })
  8. `

这个函数遍历的是 el.scopeSlots 这个数组,或许你不知道这个数组是什么内容?

同样给个例子,这里有两个 slot

经过 parse 解析之后成一个 ast,是这样的

</>复制代码

  1. {
  2. tag:"test",
  3. scopedSlots:[{
  4. slotScope: "arr"
  5. slotTarget: ""a""
  6. tag: "div"
  7. },{
  8. slotScope: "arr"
  9. slotTarget: ""b""
  10. tag: "div"
  11. }]
  12. }

没错,遍历的就是上面对象里面的 scopedSlots 数组,数组中的每一项都是一个多带带的 slot

然后会使用 genScopeSlot 去多带带处理一下,上面有放出源码

处理完之后,形成一个新的数组,genScopeSlot 也没什么好说的

拼接分类型,需要判断 slot 位置的标签是不是 template

如果是template,那么他的真实slot 是 template 的子节点,直接获取他的子节点

如果不是template,那么本身就是真实的slot

因为 template 是Vue 自带的一个 模板节点,是不存在的

拼接组件VModel

没错,这里的 model,只是属于 组件的 v-model

</>复制代码

  1. if (el.model) {
  2. data += `model: {
  3. value: $ { el.model.value },
  4. callback: $ { el.model.callback },
  5. expression: $ { el.model.expression },
  6. }, `
  7. }

官网说了这个是怎么用的

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件

也就是说,起始就是给组件传了一个 value,绑定了一个事件 input

也没有什么好讲的,记录下组件的 v-model 是这么拼接就好了

例子

经过 parse 解析,得到 ast

</>复制代码

  1. {
  2. tag: "test",
  3. model:{
  4. callback: "function ($$v) {num=$$v}"
  5. expression: ""num""
  6. value: "num"
  7. }
  8. }

拼接成字样变成字符串了

</>复制代码

  1. `
  2. _c("test",{
  3. model:{
  4. value:num,
  5. callback:function ($$v) {num=$$v},
  6. expression:"num"
  7. }
  8. })
  9. `
举个栗子

属性拼接呢,我们就讲完了,最后我们来看一个例子吧

下面这个模板,我们把它拼接起来

解析成下面这个 render 字符串,看懂了,你就掌握了 generate 的内容了

以后你就可以去看别人用Vue 写的打包后的代码了

甚至,你可以手动还原他,如果你闲得很,你可以自己写个方法,传入render 字符串,自动还原成 template 模板

</>复制代码

  1. ` _c("div", {
  2. attrs: {
  3. "b": "2"
  4. },
  5. domProps: {
  6. "a": 11
  7. }
  8. },[
  9. _c("test", {
  10. scopedSlots: _u([{
  11. key: "a",
  12. fn: function(arr) {
  13. return _c("strong")
  14. }
  15. }]),
  16. model: {
  17. value: (num),
  18. callback: function($$v) {
  19. num = $$v
  20. },
  21. expression: "num"
  22. }
  23. },[_c("span")])
  24. ]) `
最后

鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,领取红包

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106697.html

相关文章

  • Vue原理Compile - 源码 generate 节点拼接

    摘要:还原的难度就在于变成模板了,因为其他的什么等是原封不动的哈哈,可是直接照抄最后鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版...

    macg0406 评论0 收藏0
  • Vue原理Compile - 白话

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理白话版终于到了要讲白话的时候了 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...

    dingding199389 评论0 收藏0
  • Vue原理Compile - 源码 generate 拼接绑定的事件

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之拼接绑定的事件今天我们 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...

    OnlyMyRailgun 评论0 收藏0
  • Vue原理Compile - 源码 optimize 标记静态节点

    摘要:一旦我们检测到这些子树,我们可以把它们变成常数,这样我们就不需要了在每次重新渲染时为它们创建新的节点在修补过程中完全跳过它们。否则,吊装费用将会增加好处大于好处,最好总是保持新鲜。 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,...

    Soarkey 评论0 收藏0
  • Vue原理Compile - 源码 从新建实例到 compile结束的主要流程

    摘要:页面这个实例,按理就需要解析两次,但是有缓存之后就不会理清思路也就是说,其实内核就是不过是经过了两波包装的第一波包装在中的内部函数中内部函数的作用是合并公共和自定义,但是相关代码已经省略,另一个就是执行第二波包装在中,目的是进行缓存 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 ...

    CODING 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<