Skip to content
On this page

前言

此篇主要手写 Vue2.0 源码-组件原理

上一篇咱们主要介绍了 Vue Mixin 原理 是 Vue 初始化选项合并核心的 api 大家都知道 Vue 的一大特色就是组件化 此篇主要介绍整个组件创建和渲染流程 其中 Vue.extend 这一 api 是创建组件的核心

正文

javascript
// 全局组件
Vue.component('parent-component', {
  template: `<div>我是全局组件</div>`,
})
// Vue实例化
const vm = new Vue({
  el: '#app',
  data() {
    return {
      aa: 1,
    }
  },
  // render(h) {
  //   return h('div',{id:'a'},'hello')
  // },
  template: `<div id="a">
      hello 这是我自己写的Vue{{aa}}
      <parent-component><parent-component>
      <child-component></child-component>
      </div>`,
  // 局部组件
  components: {
    'child-component': {
      template: `<div>我是局部组件</div>`,
    },
  },
})

上面演示了最基础的全局组件和局部组件的用法 其实我们每一个组件都是一个继承自 Vue 的子类 能够使用 Vue 的原型方法

1.全局组件注册

javascript
// src/global-api/index.js

import initExtend from './initExtend'
import initAssetRegisters from './assets'
const ASSETS_TYPE = ['component', 'directive', 'filter']
export function initGlobalApi(Vue) {
  Vue.options = {} // 全局的组件 指令 过滤器
  ASSETS_TYPE.forEach((type) => {
    Vue.options[type + 's'] = {}
  })
  Vue.options._base = Vue //_base指向Vue

  initExtend(Vue) // extend方法定义
  initAssetRegisters(Vue) //assets注册方法 包含组件 指令和过滤器
}

initGlobalApi 方法主要用来注册 Vue 的全局方法 比如之前写的 Vue.Mixin 和今天的 Vue.extend Vue.component 等

javascript
// src/global-api/asset.js

const ASSETS_TYPE = ['component', 'directive', 'filter']
export default function initAssetRegisters(Vue) {
  ASSETS_TYPE.forEach((type) => {
    Vue[type] = function(id, definition) {
      if (type === 'component') {
        // this指向Vue
        // 全局组件注册
        // 子组件可能也有extend方法  Vue.component方法
        definition = this.options._base.extend(definition)
      }
      this.options[type + 's'][id] = definition
    }
  })
}

this.options._base 就是指代 Vue 可见所谓的全局组件就是使用 Vue.extend 方法把传入的选项处理之后挂载到了 Vue.options.components 上面

2.Vue.extend 定义

javascript
//  src/global-api/initExtend.js

import { mergeOptions } from '../util/index'
export default function initExtend(Vue) {
  let cid = 0 //组件的唯一标识
  // 创建子类继承Vue父类 便于属性扩展
  Vue.extend = function(extendOptions) {
    // 创建子类的构造函数 并且调用初始化方法
    const Sub = function VueComponent(options) {
      this._init(options) //调用Vue初始化方法
    }
    Sub.cid = cid++
    Sub.prototype = Object.create(this.prototype) // 子类原型指向父类
    Sub.prototype.constructor = Sub //constructor指向自己
    Sub.options = mergeOptions(this.options, extendOptions) //合并自己的options和父类的options
    return Sub
  }
}

Vue.extend 核心思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

3.组件的合并策略

javascript
// src/init.js

Vue.prototype._init = function(options) {
  const vm = this
  vm.$options = mergeOptions(vm.constructor.options, options) //合并options
}

还记得我们 Vue 初始化的时候合并 options 吗 全局组件挂载在 Vue.options.components 上 局部组件也定义在自己的 options.components 上面 那我们怎么处理全局组件和局部组件的合并呢

javascript
// src/util/index.js

const ASSETS_TYPE = ['component', 'directive', 'filter']
// 组件 指令 过滤器的合并策略
function mergeAssets(parentVal, childVal) {
  const res = Object.create(parentVal) //比如有同名的全局组件和自己定义的局部组件 那么parentVal代表全局组件 自己定义的组件是childVal  首先会查找自已局部组件有就用自己的  没有就从原型继承全局组件  res.__proto__===parentVal
  if (childVal) {
    for (let k in childVal) {
      res[k] = childVal[k]
    }
  }
  return res
}

// 定义组件的合并策略
ASSETS_TYPE.forEach((type) => {
  strats[type + 's'] = mergeAssets
})

这里又使用到了原型继承的方式来进行组件合并 组件内部优先查找自己局部定义的组件 找不到会向上查找原型中定义的组件

4.创建组件 Vnode

javascript
// src/util/index.js

export function isObject(data) {
  // 判断是否是对象
  if (typeof data !== 'object' || data == null) {
    return false
  }
  return true
}

export function isReservedTag(tagName) {
  // 判断是不是常规html标签
  // 定义常见标签
  let str = 'html,body,base,head,link,meta,style,title,' + 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' + 'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' + 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' + 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' + 'embed,object,param,source,canvas,script,noscript,del,ins,' + 'caption,col,colgroup,table,thead,tbody,td,th,tr,' + 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' + 'output,progress,select,textarea,' + 'details,dialog,menu,menuitem,summary,' + 'content,element,shadow,template,blockquote,iframe,tfoot'
  let obj = {}
  str.split(',').forEach((tag) => {
    obj[tag] = true
  })
  return obj[tagName]
}

上诉是公用工具方法 在创建组件 Vnode 过程会用到

javascript
// src/vdom/index.js

import { isObject, isReservedTag } from '../util/index'
// 创建元素vnode 等于render函数里面的 h=>h(App)
export function createElement(vm, tag, data = {}, ...children) {
  let key = data.key

  if (isReservedTag(tag)) {
    // 如果是普通标签
    return new Vnode(tag, data, key, children)
  } else {
    // 否则就是组件
    let Ctor = vm.$options.components[tag] // 获取组件的构造函数
    return createComponent(vm, tag, data, key, children, Ctor)
  }
}

function createComponent(vm, tag, data, key, children, Ctor) {
  if (isObject(Ctor)) {
    // 如果没有被改造成构造函数
    Ctor = vm.$options._base.extend(Ctor)
  }
  // 声明组件自己内部的生命周期
  data.hook = {
    // 组件创建过程的自身初始化方法
    init(vnode) {
      let child = (vnode.componentInstance = new Ctor({ _isComponent: true })) // 实例化组件
      child.$mount() // 因为没有传入el属性  需要手动挂载 为了在组件实例上面增加$el方法可用于生成组件的真实渲染节点
    },
  }

  // 组件vnode  也叫占位符vnode  ==> $vnode
  return new Vnode(`vue-component-${Ctor.cid}-${tag}`, data, key, undefined, undefined, {
    Ctor,
    children,
  })
}

改写 createElement 方法 对于非普通 html 标签 就需要生成组件 Vnode 把 Ctor 和 children 作为 Vnode 最后一个参数 componentOptions 传入

这里需要注意组件的 data.hook.init 方法 我们手动调用 child.$mount()方法 此方法可以生成组件的真实 dom 并且挂载到自身的 $el 属性上面 对此处有疑问的可以查看之前文章 初始渲染原理

5.渲染组件真实节点

javascript
// src/vdom/patch.js

// patch用来渲染和更新视图
export function patch(oldVnode, vnode) {
  if (!oldVnode) {
    // 组件的创建过程是没有el属性的
    return createElm(vnode)
  } else {
    // 非组件创建过程省略
  }
}

// 判断是否是组件Vnode
function createComponent(vnode) {
  // 初始化组件
  // 创建组件实例
  let i = vnode.data
  // 下面这句话很关键 调用组件data.hook.init方法进行组件初始化过程 最终组件的vnode.componentInstance.$el就是组件渲染好的真实dom
  if ((i = i.hook) && (i = i.init)) {
    i(vnode)
  }
  // 如果组件实例化完毕有componentInstance属性 那证明是组件
  if (vnode.componentInstance) {
    return true
  }
}

// 虚拟dom转成真实dom
function createElm(vnode) {
  const { tag, data, key, children, text } = vnode
  // 判断虚拟dom 是元素节点还是文本节点
  if (typeof tag === 'string') {
    if (createComponent(vnode)) {
      // 如果是组件 返回真实组件渲染的真实dom
      return vnode.componentInstance.$el
    }
    // 虚拟dom的el属性指向真实dom 方便后续更新diff算法操作
    vnode.el = document.createElement(tag)
    // 解析虚拟dom属性
    updateProperties(vnode)
    // 如果有子节点就递归插入到父节点里面
    children.forEach((child) => {
      return vnode.el.appendChild(createElm(child))
    })
  } else {
    //   文本节点
    vnode.el = document.createTextNode(text)
  }
  return vnode.el
}

判断如果属于组件 Vnode 那么把渲染好的组件真实 dom ==>vnode.componentInstance.$el 返回

6.组件的思维导图

component_01

小结

至此 Vue 的 组件源码已经完结 其实每一个组件都是一个个 Vue 的实例 都会经历 init 初始化方法 建议学习组件之前先把前面的系列搞懂 组件就比较容易理解了