乐趣区

关于vue.js:vue-源码解析323模板编译和组件化

vue 相干的源码剖析最初一篇, 模板编译和组件化

模板编译

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

<div>
    <h1@click="handler">title</h1>
    <p>somecontent</p>
</div>

渲染函数 render

render(h){
    return h('div',[h('h1',{on{click:this.handler}},'title'),
        h('p','somecontent')
    ])
}

模板编译的作用:

  • Vue 2.x 应用 VNode 形容视图以及各种交互,用户本人编写 VNode 比较复杂
  • 用户只须要编写相似 HTML 的代码 – Vue 模板,通过编译器将模板转换为返回 VNode 的 render 函数
  • .vue 文件会被 webpack 在构建的过程中转换成 render 函数

带编译器版本的 Vue.js 中,应用 template 或 el 的形式设置模板

例如:

 <div id="app">
    <h1>Vue<span> 模板编译过程 </span></h1>
    <p>{{msg}}</p>
    <comp @myclick="handler"></comp>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    Vue.component('comp', {template: '<div>I am a comp</div>'})
    const vm = new Vue({
      el: '#app',
      data: {msg: 'Hello compiler'},
      methods: {handler () {console.log('test')
        }
      }
    })
    console.log(vm.$options.render)
  </script>

编译出:

      (function anonymous(){with(this) { 
          return _c(
            "div",
            {attrs: { id: "app"} },
            [_m(0),
              _v(""), 
              _c("p", [_v(_s(msg))]),
              _v(""),
              _c("comp", { on: { myclick: handler} }),
            ], 
            1
          ); 
        } 
      });

这里 this 指向了 vm,也就是 this._c ,vm._c
_c 是 createElement() 办法,定义的地位 instance/render.js 中
咱们通过我上一篇晓得 vm._c(生成虚构 dom)是对编译生成的 render 进行渲染的办法,vm.$createElement(生成虚构 dom)是对手写 render 函数进行渲染的办法.

其余相干的渲染函数(_结尾的办法定义),在 instance/render-helps/index.js 中。

这里安利一个小工具:Vue Template Explorer

Vue 2.6 把模板编译成 render 函数的工具

Vue 3.0 beta 把模板编译成 render 函数的工具

模板编译的过程,次要是三点

  • 解析
  • 优化
  • 生成

编译模板的入口:
src\platforms\web\entry-runtime-with-compiler.js

在调用 $mount 挂载时 会调用 compileToFunctions 把 template 转换成 render 函数(外部调用_c()生成虚构 dom)。

上面就是调试 compileToFunctions 来看下生成渲染函数的过程.

  • compileToFunctions: src\compiler\to-function.js
  • complie(template, options):src\compiler\create-compiler.j
  • baseCompile(template.trim(), finalOptions):src\compiler\index.js

parse: 解析器将模板解析为形象语树 AST,只有将模板解析成 AST 后,能力基于它做优化或者生成代码字符串。

src\compiler\index.js

这里再提供一个工具能够查看转换进去的失去的 AST tree

vue AST 树 js 对象

v-if/v-for 结构化指令只能在编译阶段解决,如果咱们要在 render 函数解决条件或循环只能应用 js 中的 if 和 for

 Vue.component('comp', {data:(){return {msg:'mycomp'}
      },
      render(h){if(this.msg){return h('div',this.msg)
        } 
        return h('div','bar')
      }
        
})

优化 – optimize

  • 优化形象语法树,检测子节点中是否是纯动态节点
  • 一旦检测到纯动态节点,例如:

    • hello 整体是动态节点
    • 永远不会更改的节点

      • 晋升为常量,从新渲染的时候不在从新创立节点
      • 在 patch 的时候间接跳过动态子树

生成 – generate

把形象语法树生成字符串模式的 js 代码

AST 形象语法树很大,咱们不在这里做过多学习,次要学习理念.

组件化机制

  • 组件化能够让咱们不便的把页面拆分成多个可重用的组件
  • 组件是独立的,零碎内可重用,组件之间能够嵌套
  • 有了组件能够像搭积木一样开发网页
  • 从源码的角度来剖析 Vue 组件外部如何工作

    • 组件实例的创立过程是从上而下
    • 组件实例的挂载过程是从下而上

全局组件的定义

  Vue.component('comp', {template: '<div>I am a comp</div>'})

Vue.component() 入口

创立组件的构造函数,挂载到 Vue 实例的 vm.options.component.componentName =Ctor

//src\core\global-api\index.js
// 注册 Vue.directive()、Vue.component()、Vue.filter()
initAssetRegisters(Vue)

//src\core\global-api\assets.js
if(type==='component'&&isPlainObject(definition))
{
    definition.name=definition.name||id
    definition=this.options._base.extend(definition)
}
……
// 全局注册,存储资源并赋值
// this.options['components']['comp']=Ctor
this.options[type+'s'][id]=definition

Vue.options._base=Vue

Vue.extend()

组件构造函数的创立

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheriance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {extendOptions = extendOptions || {}
    // Vue 构造函数
    const Super = this
    const SuperId = Super.cid
    // 从缓存中加载组件的构造函数
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // 如果是开发环境验证组件的名称
      validateComponentName(name)
    }

    const Sub = function VueComponent (options) {// 调用 _init() 初始化
      this._init(options)
    }
    // 原型继承自 Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 合并 options
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {initProps(Sub)
    }
    if (Sub.options.computed) {initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    // 把组件结构构造函数保留到 Ctor.options.components.comp = Ctor
    if (name) {Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    // 把组件的构造函数缓存到 options._Ctor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

组件的构造函数原型继承自 vue, 也执行了 init 办法,也就会调用 $mount()办法,也会生成一个渲染 watcher, 也就验证了之前说的一个组件对应一个渲染 watcher, 这个组件构造函数被缓存到 options._Ctor。
前面生成 vNode 时,会依据 Ctor 创立组件的 VNode.

组件创立和挂载

组件 VNode 的创立过程

  • 创立根组件,首次 _render() 时,会失去整棵树的 VNode 构造
  • 整体流程:new Vue() –> $mount() –> vm._render() –> createElement() –> createComponent()
  • 创立组件的 VNode,初始化组件的 hook 钩子函数


生成组件的 vnode 时初始化了 installComponentHooks 钩子



这里调用了 createComponentInstanceForVnode

这样咱们就能够串通了,生成虚构 dom vnode 时检测是组件 createComponent 的时候,注册 init 钩子, 而后前面调用 init 钩子时,
调用 createComponentInstanceForVnode 实例化组件,

newvnode.componentOptions.Ctor(options)

实例化完了调用:

child.$mount(hydrating?vnode.elm:undefined,hydrating)

调用组件对象的 $mount(),生成渲染 watcher, 并把组件挂载到页面

组件实例的创立和挂载过程

Vue._update() –> patch() –> createElm() –> createComponent()

在 patch 的 createElm 时

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {// 调用 init() 办法,创立和挂载组件实例
        // init() 的过程中创立好了组件的实在 DOM, 挂载到了 vnode.elm 上
        i(vnode, false /* hydrating */)
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        // 调用钩子函数(VNode 的钩子函数初始化属性 / 事件 / 款式等,组件的钩子函数)initComponent(vnode, insertedVnodeQueue)
        // 把组件对应的 DOM 插入到父元素中
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

在这里咱们看到执行了 init,init() 的过程中创立好了组件的实在 DOM, 挂载到了 vnode.elm 上. 挂载到界面上,vnode.elm 时对界面实在 dom 的援用.

这样联合咱们三篇,就能够看到 vue 的加载运行了, 其实次要是后面两篇,
最初一篇只是在后面两篇,根底上加了组件在 vNode 中的渲染机会.

也就是说生成虚构 dom 时,判断是组件的状况时, 把对应地位的 vNode 传递, 而后调用组件继承自 vue 原型的 init 办法初始化,而后再调用 $mount 办法挂载到对应 vNode.elm 实在 dom 上.

好了,记得有点含糊.

有工夫再改下把.

本文内容借鉴于拉钩大前端训练营

退出移动版