Vue源码阅读(20):v-text、v-html、v-pre、v-once 指令的源码解析

 我的开源库:

今天解析 v-text、v-html、v-pre、v-once 等指令的底层实现原理,以具体的例子为出发点进行讲解。

1,v-text

v-text 的官方文档点击这里

v-text 的底层实现原理是更新目标元素的 textContent 属性。

首先看下例子:

new Vue({
  el: '#app',
  data() {
    return {
      name: 'tom'
    }
  },
  template: `
    <div id="app">
      <h1 v-text="name"></h1>
    </div>
  `
})

该例子渲染的页面如下所示:

1-1,模板字符串 >>> 抽象语法树

解析出来的抽象语法树如下所示:

{
  attrs: [{name: "id", value: "\"app\""}],
  attrsList: [{name: "id", value: "app"}],
  attrsMap: {
    id: "app"
  },
  children: [
    {
      attrsList: [{name: "v-text", value: "name"}],
      attrsMap: {v-text: "name"},
      children: [],
      directives: [
        {name: "text", rawName: "v-text", value: "name", arg: null, modifiers: undefined}
      ],
      hasBindings: true,
      plain: false,
      tag: "h1",
      type: 1
    }
  ],
  parent: undefined,
  plain: false,
  tag: "div",
  type: 1
}

解析出来的抽象语法树与 v-text 指令有关的内容是 h1 AST 节点中的 directives 数组中的一个对象。

directives: [
  {name: "text", rawName: "v-text", value: "name", arg: null, modifiers: undefined}
],

该对象中的内容将用于 v-text 有关代码字符串的生成。

1-2,抽象语法树 >>> 代码字符串

上面抽象语法树生成的代码字符串如下所示:

with(this){
  return _c(
    'div',
    {attrs:{"id":"app"}},
    [_c(
      'h1',
      {domProps:{"textContent":_s(name)}}
      )
    ]
  )
}

与 v-text 有关的代码字符串是:

{domProps:{"textContent":_s(name)}}

生成这段代码字符串的 Vue 源码在 genData() 中。

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // 为 el 添加 props 属性,值是 [{name: "textContent", value: "_s(name)"}]
  const dirs = genDirectives(el, state)
  if (el.props) {
    // 如果 el 有 props 属性的话,拼接 domProps:{"textContent":_s(name)} 代码字符串
    data += `domProps:{${genProps(el.props)}},`
  }

  return data
}

1-3,代码字符串 >>> vnode

生成的简要 vnode 如下所示:

{
  children: [{
    data: {
      domProps: {textContent: "tom"}
    },
    tag: "h1"
  }],
  data: {
    attrs: {id: "app"}
  },
  isRootInsert: true,
  tag: "div"
}

与 v-text 有关的属性是:

data: {
  domProps: {textContent: "tom"}
}

我们知道,vnode 是 render 函数结合当前的状态所生成的,所以,如上面所示,_s(name) 已经被替换成了 "tom" 字符串。

1-4,vnode 渲染到页面上

接下来看看 domProps: {textContent: "tom"} 是如何渲染到页面上的。

1-4-1,src/core/vdom/patch.js >>> createElm()

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
  // data == { domProps: {textContent: "tom"} }
  const data = vnode.data
  // 创建 h1 AST 节点的真实 DOM,并将其赋值到 vnode.elm 上
  vnode.elm = vnode.ns
    ? nodeOps.createElementNS(vnode.ns, tag)
    : nodeOps.createElement(tag, vnode)

  if (isDef(data)) {
    // 调用上一篇博客中说的 cbs.create 函数数组中的函数
    invokeCreateHooks(vnode, insertedVnodeQueue)
  }
  // 将 h1 真实 DOM 插入到父节点中
  insert(parentElm, vnode.elm, refElm)
}

1-4-2,src/core/vdom/patch.js >>> invokeCreateHooks()

cbs.create 是一个数组,数组中的内容是一个个的函数,具体内容如下图所示:

invokeCreateHooks 函数的作用就是遍历触发执行 cbs.create 数组中的函数。在这里,与本节内容有关的是数组中的第四个函数,他的作用是处理 DOM 的 props。

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

 1-4-3,src/platforms/web/runtime/modules/dom-props.js >>> updateDOMProps()

function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
    return
  }
  let key, cur
  const elm: any = vnode.elm
  const oldProps = oldVnode.data.domProps || {}
  let props = vnode.data.domProps || {}

  for (key in oldProps) {
    if (isUndef(props[key])) {
      elm[key] = ''
    }
  }
  for (key in props) {
    cur = props[key]
    // key:"textContent"
    // cur:"tom"
    elm[key] = cur
  }
}

该函数的最后,执行 elm[key] = cur,也就是执行 h1Element.textContent = "tom",将 "tom" 字符串设置到了 h1 元素的内部,实现目标。

2,v-html

v-html 的官方文档点击这里

v-html 的底层实现原理是更新目标元素的 innerHTML 属性,和 v-text 几乎一模一样,唯一的差别是 v-html 最终生成的 vnode 是 domProps: {innerHTML: "<span>Hello span</span>"},例如下面的模板字符串。

new Vue({
  el: '#app',
  data() {
    return {
      htmlContent: '<span>Hello span</span>'
    }
  },
  template: `
    <div id="app">
      <h1 v-html="htmlContent"></h1>
    </div>
  `
})

最终生成的 vnode 是:

{
  children: [{
    data: {
      domProps: {innerHTML: "<span>Hello span</span>"}
    },
    tag: "h1"
  }],
  data: {
    attrs: {id: "app"}
  },
  isRootInsert: true,
  tag: "div"
}

v-html 将内容渲染到页面上的源码和 v-text 一样。

function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
    return
  }
  let key, cur
  const elm: any = vnode.elm
  const oldProps = oldVnode.data.domProps || {}
  let props = vnode.data.domProps || {}

  for (key in oldProps) {
    if (isUndef(props[key])) {
      elm[key] = ''
    }
  }
  for (key in props) {
    cur = props[key]
    // key:"innerHTML"
    // cur:"<span>Hello span</span>"
    elm[key] = cur
  }
}

3,v-pre

v-pre 的官方文档点击这里

v-pre 的作用是跳过这个元素和它的子元素的编译过程,底层实现的源码很简单,主要看解析器的处理。

在这里,分为三种情况进行讨论:

  • 使用了 v-pre 指令的元素
  • 当前节点是元素节点,父级使用了 v-pre 指令
  • 当前节点是文本节点,父级使用了 v-pre 指令

3-1,源码解析

3-1-1,使用了 v-pre 指令的元素

对应的源码是:src/compiler/parser/index.js >>> parse() >>> start(),源码解析都在注释中。

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  let root
  // inVPre 是一个重要的标志变量,如果它为 true 的话,
  // 说明当前处理的节点使用了 v-pre,或者当前节点的父节点使用了 v-pre
  let inVPre = false

  // 调用 parseHTML 开始解析模板字符串
  parseHTML(template, {
    // 解析开始标签
    start (tag, attrs, unary) {
      // 此时 inVPre == false,所以需要先对 v-pre 进行解析,看看当前的元素有没有使用 v-pre  
      if (!inVPre) {
        // 进行 v-pre 指令的解析
        processPre(element)
        // 如果 element.pre 为 true 的话,说明当前的节点使用了 v-pre 指令
        // 此时需要将 inVPre 标识变量设置为 true,这很重要
        if (element.pre) {
          inVPre = true
        }
      }  
      // 接下来就用到 inVPre 变量了
      // 如果 inVPre 为 true 的话,则当前元素开始标签上的特性就不用解析了
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // 如果 inVPre 为 false 的话,则需要对开始标签上的 v-for、v-if、v-once 等内容进行解析
        // 处理 v-for
        processFor(element)
        // 处理 v-if
        processIf(element)
        // 处理 v-once
        processOnce(element)
        // element-scope stuff
        processElement(element, options)
      }          
    }
  })

  return root
}

// 用于解析 el 节点有没有使用 v-pre 指令
function processPre (el) {
  if (getAndRemoveAttr(el, 'v-pre') != null) {
    // 如果使用了的话,el.pre 设为 true
    el.pre = true
  }
}

// 该函数只是简单的将开始标签上的内容转换成对象数组的形式,并赋值到 el.attrs 上
function processRawAttrs (el) {
  const l = el.attrsList.length
  if (l) {
    const attrs = el.attrs = new Array(l)
    for (let i = 0; i < l; i++) {
      attrs[i] = {
        name: el.attrsList[i].name,
        value: JSON.stringify(el.attrsList[i].value)
      }
    }
  } else if (!el.pre) {
    // non root node in pre blocks with no attributes
    el.plain = true
  }
}

3-1-2,当前节点是元素节点,父级使用了 v-pre 指令

此处的涉及到的源码和 3-1 一样,不过执行过程不太一样,因为在执行 start() 方法时,inVPre 变量已经是 false 了。

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  let root
  // 因为当前处理节点的父节点使用了 v-pre,所以在 start 方法中,inVPre 为 true
  let inVPre = false

  // 调用 parseHTML 开始解析模板字符串
  parseHTML(template, {
    // 解析开始标签
    start (tag, attrs, unary) {
      // 此时 inVPre == true,所以无需对 v-pre 进行解析
      if (!inVPre) {
        processPre(element)
        if (element.pre) {
          inVPre = true
        }
      }  

      // inVPre == true
      if (inVPre) {
        // 因为 inVPre == true,所以代码执行到这里,并不对子节点开始标签中的内容进行解析。
        // 以此就实现了:跳过子元素编译的效果
        processRawAttrs(element)
      } else if (!element.processed) {
        processFor(element)
        processIf(element)
        processOnce(element)
        processElement(element, options)
      }          
    }
  })

  return root
}

3-1-3,当前节点是文本节点,父级使用了 v-pre 指令

对应的源码是:src/compiler/parser/index.js >>> parse() >>> chars(),源码解析都在注释中。

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  let root
  // 因为当前处理节点的父节点使用了 v-pre,所以在 chars 方法中,inVPre 为 true
  let inVPre = false

  // 调用 parseHTML 开始解析模板字符串
  parseHTML(template, {
    // 解析文本内容
    chars (text: string) {
      // 获取到父元素的 children 属性
      const children = currentParent.children
      text = inPre || text.trim()
        ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
        : preserveWhitespace && children.length ? ' ' : ''
      if (text) {
        let expression
        
        // 下面是重点,此时 inVPre == true,所以代码会执行 else if 分支
        // 即使文本使用了 Mustache 标签,例如 {{name}},也会进入到 else if 分支
        if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
          children.push({
            type: 2,
            expression,
            text
          })
        } else if (text !== ' ') {
          // 在该分支中,任何文本都会被当做普通文本(type == 3) push 到 children 数组中
          // 以此就实现了,父节点使用了 v-pre,文本子节点即使使用了 Mustache 标签,也不会被解析的效果
          children.push({
            type: 3,
            text
          })
        }
      }
    },    
  })

  return root
}

3-2,案例分析

以下面的代码为例:

new Vue({
  el: '#app',
  data() {
    return {
      name: 'tom',

      activeColor: 'red',
      fontSize: 30
    }
  },
  template: `
    <div id="app">
      <h1 v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }" v-pre>{{name}}</h1>
    </div>
  `
})

代码字符串 >>> 抽象语法树

可以发现 h1 标签的 v-bind:style 没有被解析,只是作为普通的属性放置在 attrs、attrsList、attrsMap 中。而且 h1 标签中的文本节点 type 是 3,这是普通的文本节点类型,不会被解析。这也就实现了 v-pre 指令会跳过元素和子元素编译过程的效果。

{
  attrs: [{name: "id", value: "\"app\""}],
  attrsList: [{name: "id", value: "app"}],
  attrsMap:: {id: "app"},
  children: [{
    attrs: [{name: "v-bind:style", value: "\"{ color: activeColor, fontSize: fontSize + 'px' }\""}],
    attrsList: [{name: "v-bind:style", value: "{ color: activeColor, fontSize: fontSize + 'px' }"}],
    attrsMap: {
      v-bind:style: "{ color: activeColor, fontSize: fontSize + 'px' }",
      v-pre: ""
    },
    children: [{type: 3, text: "{{name}}", static: true}],
    pre: true,
    static: true,
    tag: "h1",
    type: 1
  }],
  parent: undefined,
  plain: false,
  static: true,
  staticInFor: false,
  staticProcessed: true,
  staticRoot: true,
  tag: "div",
  type: 1
}

4,v-once

v-once 的官方文档点击这里

v-once 可以使元素和组件只渲染一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。底层源码的实现主要看生成的代码字符串,我们以一个例子开始解析。

new Vue({
  el: '#app',
  data() {
    return {
      name: 'tom'
    }
  },
  mounted(){
    setInterval(() => {
      this.name = `tom-${Math.random()}`
    }, 1000)
  },
  template: `
    <div id="app">
      <h1 v-once>{{name}}</h1>
    </div>
  `
})

4-1,代码字符串 >>> 抽象语法树

生成的抽象语法树如下图所示:

 与 v-once 有关的属性是:h1 AST 节点中的 once 属性为 true,这将作为一个标识用于生成对应的代码字符串。

4-2,抽象语法树 >>> 代码字符串

生成的代码字符串如下图所示:

这里,我们发现,除了 render 代码字符串,还有一个 staticRenderFns,这个属性是一个字符串数组,数组中的字符串也是一个个的代码字符串。

Vue 中的静态根节点和 v-once 节点有他们自己专用的代码字符串,这些代码字符串都存储在 staticRenderFns 数组中,在 render 函数初次执行的时候,可以通过 _m(index) 调用这些静态节点的代码字符串生成对应的 vnode。这些静态节点的代码字符串只会在初次渲染的时候执行渲染页面一次,当组件重新渲染的时候,对这些静态节点会直接跳过,不予处理,因为静态节点的内容是不会改变的,以此能够提高性能。

所以 v-once 的本质就是:将使用了 v-once 的元素和组件看做静态节点一样进行处理。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值