vue2 源码学习-编译器与组件挂载

273 阅读3分钟

1.模板编译

1.1定义

主要将模板template 转化为 渲染函数 render。

  • 由于vue需要使用vnode做比较更新,所以需要用到render来返回vnode
  • 为了方便用户生成render函数,才使用了template。
  • 编译器作用就是把定义好的template自动转化为render。

Vue.js 提供了Compiler + Runtime 和 Runtime only 两个模式

  1. Compiler + Runtime 是包含编译代码的,可以把编译过程放在运行时做,
  2. Runtime only后者是不包含编译代码的,需要借助 webpack 的 vue-loader事先把模板一次性编译成 render函数使用,当然我们也可以直接用render函数开发。

1.2入口

//platforms\web\entry-runtime-with-compiler.js
import { compileToFunctions } from './compiler/index'

render转化后

//src\core\instance\render-helpers\index.js
(function anonymous() {
with(this){return _c('div',{attrs:{"id":"demo"}},[
*c('h1',[* v("xxxxx")]),
*v(" "),* c('p',[*v(* s(foo))]),
*v(" "),* c('comp')],1)}
})
  • 元素节点使⽤createElement创建,别名_c
  • 本⽂节点使⽤createTextVNode创建,别名_v
  • 表达式先使⽤toString格式化,别名_s

1.3三个步骤

1.3.1 解析

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

src/compiler/parser/index.js,

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

1.3.2 优化

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

src/compiler/optimizer.js

标记静态⼦树的好处:

  • 每次重新渲染,不需要为静态⼦树创建新节点
  • 虚拟DOM中patch时,可以跳过静态⼦树

1.3.3 代码⽣成

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

generate⽅法⽣成渲染函数代码

//src/compiler/codegen/index.js

2.组件化机制

2.1组件声明

组件注册使⽤extend⽅法将配置转换为构造函数并添加到components选项
//src/shared/constants.js
export const SSR_ATTR = 'data-server-rendered'

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

//src/core/global-api/assets.js
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
   //注册三个重要的方法 components directives filters
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          //这里通过extend 生成对应的构造函数
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        //最终输出
        //components:{
        //      'my-component': {
        //          template: '<div>children component!</div>'
        //      }
        //  }
        //这里的 definition 是一个构造函数
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

//src/core/global-api/index.js 
//开始注册相关方法
initAssetRegisters(Vue) 

2.2 组件实例创建及挂载

  • 观察⽣成的渲染函数

          对应的render返回的渲染函数是_c("comp") ,//这里comp是自定义标签做特殊处理
          
    

2.2.1创建组件VNode

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

_createElement实际执⾏VNode创建的函数,由于传⼊tag是⾮保留标签,因此判定为⾃定义组件通过

createComponent去创建

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

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

2.2.2创建组件实例

根组件执⾏更新函数时,会递归创建⼦元素和⼦组件,⼊⼝createElm

createEle() core/vdom/patch.js

⾸次执⾏_update()时,patch()会通过createEle()创建根元素,⼦元素创建研究从这⾥开始

createComponent core/vdom/patch.js

2.3 代码分析


//src/core/instance/render.js
export function initRender (vm: Component) {
   ... 
   //通过函数科里化 挂载两个h函数
   //这个用于编译器生成
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  //这个用于用户自定义生成
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}


//src/core/vdom/create-element.js
//参数为 tag data 和child 返回一个vnode
//这个会处理保留的标签和自定义标签

import { createComponent } from './create-component'

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
    ...
    
      let vnode, ns
      //这个判断是否为 字符串标签 
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {//这里是系统保留标签 如 h1 div
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      //这里开始实例化vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(
    //这里通过resolveAsset 在$options里的components里面拿到对应的信息并返回构造函数
    Ctor = resolveAsset(context.$options, 'components', tag))
    ) {
    
      // component 这个是自定义标签,调用createComponent方法创建
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
  //处理传入的是 comp组件标签 h(comp)情况
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
}


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

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }
  
  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  } 
    
  // install component management hooks onto the placeholder node
  //这里注册各个重要钩子方法
   //定义各种钩子init/prepatch/insert/destroy
  installComponentHooks(data)
  
  return vnode
}


function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}


const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
    //这里是初始化处理
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
    //这里是第一次初始化处理,这个通过vnode转化为组件的实例化
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      //这里会最终挂在子节点到父节点上。但是由于父节点为挂到根的树上,所以还不能显示在页面上
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  insert (vnode: MountedComponentVNode) {
      ...
  },

  destroy (vnode: MountedComponentVNode) {
      ...
  }
}


//src/core/vdom/patch.js
export function createPatchFunction (backend) {
 ...
 function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    //这里通过createComponent 创建对应的组件
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    如果不是自定义组组件 下面正常处理保留标签 如h1 div
       const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
        ...
      if (__WEEX__) { 
             ...
      } else {
      //通过createChildren 实现递归继续创建子节点
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        //插入子节点
        insert(parentElm, vnode.elm, refElm)
      }
        
    }
    
    
    ...
   }
   
  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data //vnode.data保留了 前面init初始化的一系列钩子
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      //这里
      if (isDef(i = i.hook) && isDef(i = i.init)) {
         //执行子组件的实例创建与挂载
        i(vnode, false /* hydrating */)//这里创建了dom
      }
      // 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)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)//插入当前对应的父节点
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }
}

2.4流程

整体流程:

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

=> vm._update() => patch() => createElm => createComponent()

  1. 全局注册组件Vue.component(id,{})

  2. 创建虚拟dom实例化(vnode)createElement() -> createComponent() 创建组件

     //src/core/vdom/create-component.js -createComponent() 组件vnode创建
     定义各种钩子init/prepatch/insert/destroy
     
    
  3. 把vnode 转化为真实的dom,并且与挂载

       //src/core/vdom/patch.js -createComponent() 创建组件实例并挂载,vnode转换为dom