Vue2.0 中模板编译的过程学习

16 篇文章 1 订阅

要说vue 和 react 最大的不同之一,我肯定会说 vue 是开箱即用的,也就是有的情况下,不需要 babel 转义就能直接运行,就好像是一个高级的 jquery 包一样

1、入口 $mount

光光看目录的名称就可以知道,这里是和平台有关的代码,也就是运行在 浏览器的代码,这里主要是 改造 $mount 函数

/* @flow */

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

// 获取 id 的同时,缓存一下 对应的 template
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

// 缓存 公共的 $mount 用来重写,方便稍后调用
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  //  如果是 body 标签 或者 html 标签,就返回 错误
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  // 把 template 转换为 render 函数
  // 也就是说,如果设置了 render 函数,就直接执行 render 
  if (!options.render) {
    let template = options.template
    if (template) {
      // 如果 template 是 字符串的话,先判断是不是 id
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
        // 是 node 的话,直接获取 innerHTML
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 获取 outerHTML 作为模板
      template = getOuterHTML(el)
    }
    if (template) {
      // 整个编译的重点就是在这里了,这里稍后解释
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 这里就获取到 了 render 函数
      options.render = render
      // 静态节点的渲染函数
      options.staticRenderFns = staticRenderFns
    }
  }
  // 再在这里调用 全平台的 mount 函数
  return mount.call(this, el, hydrating)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
// 获取 outerHTML 
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

export default Vue
  1. 首先缓存 全平台的 mount 函数,并且重写 vue 的 mount 函数
  2. 如果传入了 el 参数的话,先获取 el 参数,如果 传进来的节点 是 body 或者 html 节点,那就 报错并停止执行
  3. 如果 没有传入 render 函数的话,检查 template 是否传入,并且获得对应的模板
  4. 如果 render 和 template 都没有传入,就 获取 传入 el 节点的 outerHTML 作为 template 模板
  5. 然后 把 template 执行 compileToFunctions 函数生成 render 函数,作为参数 放进 options 之中
  6. 最后调用 第一步缓存的 mount 函数,并且返回
  7. 所以 根据传入的参数,其实这里获取的 执行模板是有 优先级的 render > template > el

2、compileToFunctions 

这个函数就很简单了,只是贴一下代码,直接进入 createCompiler 函数

/* @flow */

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

3、baseCompile 编译优化 模板的过程

/* @flow */

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 把模板编译成 AST 抽象语法树
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 优化抽象语法树
    optimize(ast, options)
  }
  // 把抽象语法树 转换成 js 的 代码
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,  // 这里的 render 是字符串形式的,需要使用 new Function 才行
    staticRenderFns: code.staticRenderFns
  }
})
  1. 先不管 外面的 createCompilerCreator,这个函数主要是处理一些配置和错误捕获用的 ,只关注 当前的 baseCompile 函数就好

  2. 把 template 模板编译成 AST 抽象语法树 (这一步过于复杂,先过了)

  3. optimize 优化 抽象语法树 --> 这个过程 就是给 抽象语法树 增加静态节点 标志 isStatic ,如果熟悉 vue diff 的同学,肯定会记得 有一步 是判断 isStatic,然后直接就不比较了,这样可以优化 diff 的性能

  4. 把 抽象语法树 转换为 js 代码,注意的是,这里的 js 代码是字符串形式的,如下图所示

4、optimize 优化抽象语法树,给其 增加 isStatic 参数,方便后期 diff

/* @flow */

import { makeMap, isBuiltInTag, cached, no } from 'shared/util'

let isStaticKey
let isPlatformReservedTag

const genStaticKeysCached = cached(genStaticKeys)

// 用来标记 静态子树 
export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  // 标记静态节点
  markStatic(root)
  // second pass: mark static roots.
  // 标记静态根节点
  markStaticRoots(root, false)
}

function genStaticKeys (keys: string): Function {
  return makeMap(
    'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
    (keys ? ',' + keys : '')
  )
}

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {  // 元素节点
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    // 不要将组件插槽内容设为静态。这样可以避免
    //1. 无法更改节点的插槽
    //2. 静态插槽内容无法进行热重新加载
    if (
      // export const isReservedTag = (tag: string): ?boolean => {
      //   return isHTMLTag(tag) || isSVG(tag)
      // }
      !isPlatformReservedTag(node.tag) &&  // 不是 HTML 标签,也就是说,是组件
      node.tag !== 'slot' &&               // 当前的 tag 标签 不是 slot
      node.attrsMap['inline-template'] == null  // 是否是 内嵌模板
    ) {
      return   // 如果当前是组件节点,而且是 slot 的话,继续执行
    }
    // 递归调用子节点
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {  // 如果当前有一个子节点 不是静态的,那这个节点就不是  static 的
        node.static = false
      }
    }
    // 处理 if 
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {  // 元素节点,而且 属性是否有 static 和 once
    if (node.static || node.once) {
      node.staticInFor = isInFor // 在 for 循环中 是否是 静态的
    }
    // For a node to qualify as a static root, it should have children that
    // are not just static text. Otherwise the cost of hoisting out will
    // outweigh the benefits and it's better off to just always render it fresh.
    // 如果 只有 一个子节点, 而且是 文本节点,那么这个就不是静态的
    // 收益大于支出
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    // 遍历所有的子节点
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

function isStatic (node: ASTNode): boolean {
  // 表达式
  if (node.type === 2) { // expression
    return false
  }
  // 文本内容
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || (  // 是否是 v-pre (跳过当前节点的编译)
    !node.hasBindings && // 没有 动态绑定的节点
    !node.if && !node.for && // 不是 v-if v-for
    !isBuiltInTag(node.tag) && // 不是 内部组件,slot component 组件
    isPlatformReservedTag(node.tag) && // 是 html 标签,也就是说,不是 组件
    !isDirectChildOfTemplateFor(node) &&   // 父组件是否是 v-for
    Object.keys(node).every(isStaticKey)  // 每个属性是否都在  'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticClass,staticStyle' 中
  ))
}

function isDirectChildOfTemplateFor (node: ASTElement): boolean {
  while (node.parent) {
    node = node.parent
    // 如果父组件 不是 template ,返回false
    if (node.tag !== 'template') {
      return false
    }
    // 如果父组件 是 v-for 返回  true
    if (node.for) {
      return true
    }
  }
  return false
}
  1. 调用 markStatic 标记当前节点是否是 静态节点
  2. 调用 markStaticRoots 当前 节点是否是 静态 根节点

markStatic

  1. 调用 isStatic  函数判断当前节点是否是 静态节点
  2. 如果当前是 表达式,就不是静态的,如果当前是 一个 文本节点,那就是 静态的
  3. 如果当前是 一个 node 节点的话
  4. 查看是否标记了 v-pre (跳过当前节点的 编译 )
  5. 没有动态绑定的节点 && 不是 v-if v-for && 不是 内部的 slot/component 组件 && 是 html 节点(不是组件) && 父组件不是 template && 父组件 不是 v-for && 每个属性 都在  [type, tag, attrsList, attrsMap, plain, parent, children, attrs, start, end, rawAttrsMap, staticClass, staticStyle ] 中
  6. 接下来进一步 解析 static
  7. 判断是否是 组件的 slot ,是的话,直接跳过(在第5步中,遇到 slot 则不设置 static ),所以 slot 就不会被设置为 静态节点
  8. 递归调用子节点,然后 通过 markStatic 进行标记
  9. 如果当前有一个子节点 不是静态的,那这个父节点就不是 static 的

  10. 标记 v-if 节点是否是 静态节点

markStaticRoots

  1. 如果当前节点是 node 元素节点的话

  2. 如果 只有 一个子节点, 而且是 文本节点,那么这个就不是静态的 (英文注释写的很明白,收益大于支出,还不如每次都 动态渲染一遍)

  3. 遍历 所有的子节点,以及 v-if

5、generate 把 AST 转换为 js 代码

generate 函数 不做过多地解读,就是各种条件的判断,对 AST 的处理

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {  // v-pre 如果父节点有这个,子节点也是 静态的
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) { // 静态根节点
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {  // v-once
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {    // v-for
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {       // v-if
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {  // 当前节点是 template,而且 不是 slot,不是 v-pre
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {    // 当前节点是 slot
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {    // 当前节点是 组件
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        // 生成元素的属性/指令/事件等
        // 处理各种指令,包括 module text html
        data = genData(el, state)
      }
      // 
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

不过 读到这里,可能会对 这里出现的 _c  _l 等辅助函数感到奇怪,这个是在 vue 初始化的时候,注入进入当前实例的

_c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

export function installRenderHelpers (target: any) {  // 给 vue 的实例注入 下面的方法
  // 渲染相关,在 render 中执行
  target._o = markOnce    // .once
  target._n = toNumber    // 转换为 数字
  target._s = toString    // 转换为字符串
  target._l = renderList  // v-for
  target._t = renderSlot  // 渲染 slot,每个slot都是 一个 vnode ,获取对应的 slot 并渲染
  target._q = looseEqual  // 两个值是否大致相等,如果是对象的话,里面的属性是否相等
  target._i = looseIndexOf // 传入一个数组,一个对象,如果 数组有大致相等的对象或者值,就返回第一个
  target._m = renderStatic   // 处理静态节点
  target._f = resolveFilter  // filter 过滤器
  target._k = checkKeyCodes  // keycode
  target._b = bindObjectProps  // 处理 props 以及 .sync 修饰符
  target._v = createTextVNode  // 创建文本节点
  target._e = createEmptyVNode  // 创建空白的 vnode 节点
  target._u = resolveScopedSlots  // 作用域插槽,就是在 把 slot.fn 拿出来,作为返回值
  target._g = bindObjectListeners // 把绑定的 函数 放到 类似于 on: {click: []} 的数组中去
  target._d = bindDynamicKeys    // 处理 v-bind v-on 中的 动态属性绑定 :[key]="value" 
  target._p = prependModifier  // 可以将修饰符运行时标记动态附加到事件名称
                              // 就是 将 click.capture 标记为 !click  click.once 标记为 ~click   click.passive 标记为 &click
}

6、createFunction 将上面的 字符串 js 转换为 可以执行的 js 函数

再贴一下 返回的 js

// 通过字符串的方法,new Function 转换为 一个函数
function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

这里的 with(this) 传入的是当前的  vue 实例,作用看下图,就是 把 内部的 变量 指向 this

7、推荐几个网站

代码转换为 AST

vue 2.0 将 template 转换为 vnode 的 render 函数

vue 3.0 将 template 转换为 vnode 的 render 函数

 

 

 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值