06Vue 源码解析3

作者: LM林慕 | 来源:发表于2020-07-12 21:15 被阅读0次

    Vue 源码解析3

    模板编译

    模板编译的主要目标是将模板(template)转为渲染函数(render)

    template==>render

    模板编译必要性

    Vue2.0 需要用到 Vnode 描述视图以及各种交互,手写显然不切实际,因此用户只需编写类似 HTML 代码的 Vue 模板,通过编译器将模板转换为可返回 Vnode 的 render 函数。

    体验模板编译

    带编译器的版本中,可以使用 template 或 el 的方式生命模板,测试 demo

    (function anonymous (
      ) {
        with (this) {
          return _c('div', { attrs: { "id": "demo" } }, [_c('h1', [_v("Vue模板编
        译")]),_v(" "),_c('p',[_v(_s(foo))]),_v(" "),_c('comp')],1)}
        })
    

    输出结果大致如下:

    (function anonymous () {
      with (this) {
        return _c('div', { attrs: { "id": "demo" } }, [
          _c('h1', [_v("Vue模板编译")]),
          _v(" "), _c('p', [_v(_s(foo))]),
          _v(" "), _c('comp')], 1)
      }
    })
    
    • 元素节点使用 createElement 创建,别名 _c

    • 本文节点使用 createTextVNode 创建,别名 _v

    • 表达式先使用 toString 格式化,别名 _s

    • 其他渲染 helpers:src\core\instance\render-helpers\index.js

    整体流程

    compileToFunctions

    若指定 template 或 el 选项,则会执行编译,platforms\web\entry-runtime-with-compiler.js

    编译过程

    编译分为三步:解析、优化和生成,src\compiler\index.js

    测试 demo

    模板编译过程

    image.png

    实现模板编译共有三个阶段:解析、优化和生成。

    解析 - parse

    解析器将模板解析为抽象语法树,基于 AST 可以做优化或者代码生成工作。

    调试查看得到的 AST,/src/compiler/parser/index.js,结构如下:

    image.png

    解析器内部分了 HTML 解析器、文本解析器和过滤解析器,最主要是 HTML 解析器。

    优化 - optimize

    优化器的作用是在 AST 中找出静态子树并打上标记。静态子树是在 AST 中永远不变的节点,如纯文本节点。

    标记静态子树的好处:

    • 每次重新渲染,不需要为静态子树创建新节点;

    • 虚拟 DOM 中 patch 时,可以跳过静态子树。

    测试 demo

    代码实现,src/compiler/optimizer.js - optimize

    标记结束

    image.png

    代码生成 - generage

    将 AST 转换成渲染函数中的内容,即代码字符串。

    generate 方法生成渲染函数代码,src/compiler/codegen/index.js

    生成的 code:

    `_c('div',{attrs:{"id":"demo"}},[
    _c('h1',[_v("Vue.js测试")]),
    _c('p',[_v(_s(foo))])
    ])`
    

    典型指令的实现:v-if、v-for

    image.png

    着重观察几个结构性指令的解析过程。

    解析 v-if:parser/index.js

    processIf 用于处理 v-if 解析:

    function processIf (el) {
      const exp = getAndRemoveAttr(el, 'v-if')
      if (exp) {
        el.if = exp
        addIfCondition(el, {
          exp: exp,
          block: el
        })
      } else {
        if (getAndRemoveAttr(el, 'v-else') != null) {
          el.else = true
        }
        const elseif = getAndRemoveAttr(el, 'v-else-if')
        if (elseif) {
          el.elseif = elseif
        }
      }
    }
    

    解析结果:

    image.png

    代码生成,codegen/index.js

    genIfConditions 等用于生产条件语句相关代码。

    生成结果:

    "with(this){return _c('div',{attrs:{"id":"demo"}},[
    (foo) ? _c('h1',[_v(_s(foo))]) : _c('h1',[_v("no title")]),
    _v(" "),_c('abc')],1)}"
    

    解析 v-for:parser/index.js

    processFor 用于处理 v-for 指令:

    export function processFor (el: ASTElement) {
      let exp
      if ((exp = getAndRemoveAttr(el, 'v-for'))) {
        const res = parseFor(exp)
        if (res) {
          extend(el, res)
        } else if (process.env.NODE_ENV !== 'production') {
          warn(
            `Invalid v-for expression: ${exp}`,
            el.rawAttrsMap['v-for']
          )
        }
      }
    }
    

    解析结果:v-for="item in items"

    for:"items"

    alias:"item"

    image.png

    代码生成,src\compiler\codegen\index.js

    genFor 用于生成响应代码。

    生成结果:

    "with(this){return _c('div',{attrs:{"id":"demo"}},[_m(0),_v(" "),(foo)?_c('p',
    [_v(_s(foo))]):_e(),_v(" "),
    _l((arr),function(s){return _c('b',{key:s},[_v(_s(s))])})
    ,_v(" "),_c('comp')],2)}"
    

    v-if、v-for 这些指令只能在编译器阶段处理,如果我们要在 render 函数处理条件或循环只能使用 if 和 for。

    Vue.component('comp', {
      props: ['foo'],
      render (h) { // 渲染内容跟foo的值挂钩,只能⽤if语句
        if (this.foo == 'foo') {
          return h('div', 'foo')
        }
        return h('div', 'bar')
      }
    })
    
    (function anonymous (
    ) {
      with (this) {
        return _c('div', { attrs: { "id": "demo" } }, [_m(0), _v(" "), (foo) ? _c('p',
          [_v(_s(foo))]) : _e(), _v(" "), _c('comp')], 1)
      }
    })
    

    组件化机制

    组件声明:Vue.component()

    initAssetRegisters(Vue) src/core/global-api/assets.js,组件注册使用 extend 方法将配置转换为构造函数并添加到 components 选项。

    组件实例创建及挂载

    观察生成的渲染函数:

    "with(this){return _c('div',{attrs:{"id":"demo"}},[
      _c('h1', [_v("虚拟DOM")]), _v(" "),
      _c('p', [_v(_s(foo))]), _v(" "),
      _c('comp') // 对于组件的处理并⽆特殊之处
      ], 1)}"
    

    整体流程

    首先创建的是跟实例,首次 _render() 时,会得到整棵树的 Vnode 结构,其中自定义组件相关的主要有:

    整体流程:

    new Vue() => $mount() => vm._render(h) => createElement() => createComponent() => patch => createElm => createComponent()

    _createElement - src\core\vdom\create-element.js

    _createElement 实际执行 Vnode 创建的函数,由于传入 tag 是非保留标签,因此判定为自定义组件通过 createComponent 去创建。

    createComponent - src/core/vdom/create-component.js

    创建组件 Vnode,保存了上一步处理得到的组件构造函数,props,事件等

    创建组件实例

    根组件执行更新函数时,会递归创建子元素和子组件,入口 createElm。

    createEle() core/vdom/patch.js line751

    首次执行 _update() 时,patch() 会通过 createEle() 创建根元素,子元素创建研究从这里开始。

    createComponent core/vdom/patch.js line144

    自定义组件创建:

    // 组件实例创建、挂载
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    if (isDef(vnode.componentInstance)) {
      // 元素引⽤指定vnode.elm,元素属性创建等
      initComponent(vnode, insertedVnodeQueue)
      // 插⼊到⽗元素
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
    

    总结

    Vue源码学习使我们能够深入理解原理,解答很多开发中的疑惑,规避很多潜在的错误,写出更好的代码。学习大神的代码,能够学习编程思想,设计模式,训练基本功,提升内力。

    相关文章

      网友评论

        本文标题:06Vue 源码解析3

        本文链接:https://www.haomeiwen.com/subject/musncktx.html