【手写 Vue2.x 源码】第四十三篇 - 组件部分 - 组件相关流程总结

275 阅读6分钟

一,前言

上篇,介绍了《组件部分-组件挂载流程简述》;

至此,【Vue2.x 源码学习】专栏核心的几个专题就初步完成了,包括:

  1. 响应式数据原理(第一篇 ~ 第十篇)
  2. 模板编译原理(第十一篇 ~ 第二十篇)
  3. 依赖收集、异步更新、生命周期(第二十一篇 ~ 第二十七篇)
  4. diff 算法原理(第二十八篇 ~ 第三十三篇)
  5. 组件部分(第三十四篇 ~ 第四十二篇)

简单总结一下,专栏存在的问题及后续迭代计划:

  • 响应式数据原理部分:写的有些乱,顺序上还需要再做调整,好在事情讲清楚了;
  • 模板编译原理部分:部分内容仍需要再细化,要把流程讲清楚,让人一看就懂;
  • diff 算法部分:还需要添加一些必要图示和动画,这部分不难,但做好不容易;
  • 组件部分:近半月工作比较忙,这部分质量并不高,二轮首先优化这块;

计划在 8 月更文活动完结后,进入第二轮优化阶段;

目标:修复问题较大的几篇文章,并在过程中梳理规划出第三轮优化的内容;

收回来;本篇对组件部分的几篇文章做一次阶段性总结《组件部分-组件相关流程总结》;


二,主要流程划分

  • Vue.component 的实现
  • Vue.extend 的实现
  • 组件合并的实现
  • 组件编译的实现
  • 创建组件的虚拟节点
  • 组件生命周期的实现
  • 创建组件的真实节点
  • 组件挂载的实现

三,各流程的实现简述

1,Vue.component 的实现

  • 在 Vue 初始化流程中,会对全局 API 做集中处理,创建出 Vue.component API;
  • 将 Vue 保存到全局对象 Vue.options 上,以便在后续流程中,组件可以直接通过vm.$options._base 获取到 Vue;备注:子类组件上并没有 extend 方法,需要通过 Vue 才能获取到,再将组件定义对象处理为组件的构造函数;
  • Vue.component 中,若组件定义为对象,使用 Vue.extend 处理为组件构造函数;
  • 扩展 Vue.options 对象,将全局组件定义维护到 Vue.options.components 上;
// src/global-api/index.js
export function initGlobalAPI(Vue) {

  Vue.options = {};
  Vue.options._base = Vue; // 便于组件通过 vm.$options._base 拿到 Vue; 
  Vue.options.components = {};
  
  Vue.extend = function (definition) {}
  
  Vue.component = function (id, definition) {
  
    let name = definition.name || id;
    definition.name = name;
    
    // 处理组件定义,生成组件构造函数
    if(isObject(definition)){
      definition = Vue.extend(definition)
    }
    
    // 维护组件与构造函数的映射关系
    Vue.options.components[name] = definition;  
  }
}

Vue.options.components的作用:

  • 利用全局对象 vm.constructor.options 完成全局组件与局部组件的合并;
  • 通过组件虚拟节点的标签名,查询对应组件的构造函数,完成组件的实例化;

2,Vue.extend 的实现

  • Vue.extend:使用基础 Vue 构造器,创建一个子类;
  • Vue.extend:内部会根据组件定义生成一个继承于 Vue 原型的组件子类 Sub
  • 修复 constructor 指向问题:由于 Object.create 会产生一个新的实例作为子类的原型,导致constructor 指向错误,应指向当前子类 Sub
  • 返回组件的构造函数 SubVue.component 中将对组件构造函数进行全局映射;
// src/global-api/index.js

export function initGlobalAPI(Vue) {
  Vue.extend = function (definition) {
  
    const Super = this;
    const Sub = function (options) {
      this._init(options);
    }
    
    // 子类继承父类原型
    Sub.prototype = Object.create(Super.prototype);
    // 修复 constructor 指向问题,指向 Sub
    Sub.prototype.constructor = Sub;
    
    return Sub;
  }
}

3,组件合并的实现

  • 此时,vm.constructor.options 已经包含了 Vue.options.components 中的全局组件声明;
  • 执行 new Vue 时,会进行组件的初始化,进入 _init 方法;
  • _init 方法中,通过 mergeOptions 方法:将 new Vue 传入的局部组件定义 options 与全局组件定义进行合并操作;
  • mergeOptions 方法中,通过策略模式,获取到预设的组件合并策略函数;
  • 组件的合并策略:创建新对象继承于全局组件定义,并将局部组件定义添加到新对象中;此时会优先在新对象中查找局部组件定义,若未找到,会继续通过链上的继承关系查找全局组件定义;
// src/init.js#initMixin

Vue.prototype._init = function (options) {
    const vm = this;
    
    // 组件合并
    vm.$options = mergeOptions(vm.constructor.options, options);
    
    initState(vm);
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
}

组件的合并策略

// src/utils.js
let strats = {};  // 用于存放策略函数

// 设置组件的合并策略
strats.component = function (parentVal, childVal) {
  // 将全局组件定义放到链上
  let res = Object.create(parentVal); 
  // 将局部组件定义放到对象上
  if(childVal){
    for (let key in childVal) {
     res[key] = childVal[key];
    }
    // 优先查找局部组件定义,若未找到,会继续通过链上的继承关系查找全局组件定义;
    return res;
  }
}

// 根据合并策略进行选项的合并
export function mergeOptions(parentVal, childVal) {
  let options = {};
  for(let key in parentVal){
    mergeFiled(key);
  }
  
  for(let key in childVal){
    if(!parentVal.hasOwnProperty(key)){
      mergeFiled(key);
    }
  }

  function mergeFiled(key) {
    // 策略模式:获取当前 key 的合并策略
    let strat = strats[key];
    if(strat){  
      options[key] = strat(parentVal[key], childVal[key]);
    }else{  // 默认合并策略:新值覆盖老值
      options[key] = childVal[key] || parentVal[key];
    }
  }

  return options;
}

需要注意:

  • vm.constructor.options 中的全局组件,可能已被 Vue.extend 处理为函数(组件的构造函数);
  • options 中的局部组件,不会被 Vue.extend 处理,此时还是一个对象;

4,组件编译的实现

  • 与模板编译流程相似:组件模板 -> AST 语法树 -> render 函数

5,创建组件的虚拟节点

  • render 函数中,通过 createElement 方法:生成组件的虚拟节点;
  • createElement 方法中,进行标签筛查,若为非普通标签则视为组件;获取组件定义(有可能是构造函数),并通过 createComponent 方法,创建组件虚拟节点 componentVnode
  • createComponent 中,当获取到的组件定义 Ctor 为对象时,需先通过 Vue.extend 处理为组件的构造函数;
  • 获取事先保存在全局 vm.$options._base 中的 Vue,使用 Vue.extend 处理,生成组件构造函数;
  • 通过 vnode 方法生成组件的虚拟节点 componentVnode,将组件相关信息封装到 componentOptions 对象中;完整的 componentOptions 包括:Ctor、propsData、listeners、tag、children
// src/vdom/index.js

export function createElement(vm, tag, data={}, ...children) {

  // 处理组件类型
  if (!isReservedTag(tag)) {
    let Ctor = vm.$options.components[tag];
    // 创建组件的虚拟节点
    return createComponent(vm, tag, data, children, data.key, Ctor);
  }
  
  // 创建元素的虚拟节点
  return vnode(vm, tag, data, children, data.key, Ctor);
}

// 创建组件虚拟节点 componentVnode
function createComponent(vm, tag, data, children, key, Ctor) {

  if(isObject(Ctor)){
    // 通过 Vue.extend 创建组件的构造函数
    Ctor = vm.$options._base.extend(Ctor)
  }
  
  let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
  
  return componentVnode;
}

注意,所有组件最终都会被 Vue.extend 处理成为组件的构造函数:

  • 全局组件:在 Vue.component 内部可能已经被 Vue.extend 处理完成;
  • 局部组件:在 createComponent 创建组件虚拟节点时,被 Vue.extend 处理;

6,组件生命周期的实现

  • createComponent 方法创建组件虚拟节点过程中,通过扩展 data 属性,为组件添加生命周期钩子函数;
  • 在组件初始化时,通过执行 init 钩子函数,实现组件的实例化并完成页面挂载;
// src/vdom/index.js

function createComponent(vm, tag, data, children, key, Ctor) {

  if(isObject(Ctor)){
    Ctor = vm.$options._base.extend(Ctor)
  }
  
  // 扩展组件的生命周期
  data.hook = {
    init(){
      console.log("Hook-init:执行组件实例化并完成挂载");
      // 注意:此处的 vm 不是组件实例,需将当前组件实例存取来
      let child = vnode.componentInstance = new Ctor({});
      child.$mount();
    },
    prepatch(){},
    postpatch(){}
  }
  
  let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
  
  return componentVnode;
}

注意:将组件实例保存到虚拟节点上 vnode.componentInstance,便于后续获取组件真实节点,完成组件的挂载操作;


7,创建组件的真实节点

  • createElm 方法中,通过执行 createComponent 方法,将组件虚拟节点生成真实节点并返回;

备注:createComponent 执行完毕后,vnode.componentInstance 赋值为组件实例,vnode.componentInstance.$el 即为组件的真实节点;

// 根据虚拟节点创建真实节点(递归)
export function createElm(vnode) {

  let { tag, data, children, text, vm } = vnode;
  
  if (typeof tag === 'string') {
    if(createComponent(vnode)){// 组件处理:根据组件的虚拟节点创建真实节点
      return vnode.componentInstance.$el;
    }
    
    vnode.el = document.createElement(tag) 
    updateProperties(vnode, data)
    children.forEach(child => {
      vnode.el.appendChild(createElm(child))
    });
    
  } else {
    vnode.el = document.createTextNode(text)
  }
  
  return vnode.el;
}
  • createComponent 方法中,若存在 hook 即为组件,通过组件 init 钩子函数,进行组件初始化操作;
// 根据组件的虚拟节点创建真实节点
function createComponent(vnode) {
  let i = vnode.data;
  // 先确定有 hook;再拿到 init 方法;
  if((i = i.hook)&&(i = i.init)){
    i(vnode); // 使用 init 方法处理 vnode
  }
}
  • 组件的 init 钩子函数中,通过 new Ctor 实例化组件时,会执行 _init 进行组件的初始化,此时,vm.$options.el 为空,不会自动挂载组件;
  Vue.prototype._init = function (options) {
    const vm = this;
    vm.$options = mergeOptions(vm.constructor.options, options);
    initState(vm);
    
    // 由于 el 不存在,所以不会执行 vm.$mount
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
  • 通过 child.$mount() 进行组件挂载操作,由于 $mount 参数 elnull,所以也不会进行挂载;
  Vue.prototype.$mount = function (el) {
    const vm = this;
    const opts = vm.$options;
    el = document.querySelector(el);
    vm.$el = el;
    
    if (!opts.render) {
      let template = opts.template;
      if (!template) {
        template = el.outerHTML;
      }
      let render = compileToFunction(template);
      opts.render = render;
    }
    
    mountComponent(vm);
  }
  • 生成组件 render 函数后,执行 mountComponent 进行组件的挂载;
// src/lifeCycle.js

export function mountComponent(vm) {

  let updateComponent = ()=>{
    vm._update(vm._render());  
  }
  
  callHook(vm, 'beforeCreate');
  
  new Watcher(vm, updateComponent, ()=>{
    callHook(vm, 'created');
  },true)
  
  callHook(vm, 'mounted');
}
  • updateComponent 中,通过 _render 产生组件虚拟节点:
  Vue.prototype._render = function () {
    const vm = this;
    let { render } = vm.$options;
    let vnode = render.call(vm);
    return vnode
  }
  • vm.render 执行完成后,继续执行 _update 方法:
// src/lifeCycle.js

export function lifeCycleMixin(Vue){
  Vue.prototype._update = function (vnode) {
    const vm = this;
    let preVnode = vm.preVnode;
    vm.preVnode = vnode;
    
    if(!preVnode){// 初渲染
      // 传入当前真实元素vm.$el,虚拟节点vnode,返回真实节点
      vm.$el = patch(vm.$el, vnode);
    }else{// 更新渲染:新老虚拟节点做 diff 比对
      vm.$el = patch(preVnode, vnode);
    }
  }
}
  • 初渲染 preVnode 为空,patch 方法中 oldVnodenull(组件的 el 为空),使用组件的虚拟节点,创建出组件的真实节点并返回:
export function patch(oldVnode, vnode) {
  if(!oldVnode){// 组件挂载流程
    return createElm(vnode);  // 直接使用组件虚拟节点创建真实节点
  }
  • 返回的 vm.$el 即为组件的真实节点;

8,组件挂载的实现

  • createElm 方法内中,会递归的生成真实节点,并插入对应的父节点中;
  • createElm 为深度优先遍历,最终将完整的 div 挂载到页面上;
// 根据虚拟节点创建真实节点(递归)
export function createElm(vnode) {

  let { tag, data, children, text, vm } = vnode;
  
  if (typeof tag === 'string') {
    if(createComponent(vnode)){// 组件处理:根据组件的虚拟节点创建真实节点
      return vnode.componentInstance.$el;
    }
    
    vnode.el = document.createElement(tag) 
    updateProperties(vnode, data)
    // 将真实节点插入到对应的父节点中
    children.forEach(child => {
      vnode.el.appendChild(createElm(child))
    });
    
  } else {
    vnode.el = document.createTextNode(text)
  }
  
  return vnode.el;
}

备注:

_update 执行完成,继续回到 mountComponent 方法,执行 beforeCreate 钩子、生成组件独立的渲染 watcher、执行 mounted 钩子,完成组件挂载;

// src/lifeCycle.js
export function mountComponent(vm) {

  let updateComponent = ()=>{
    vm._update(vm._render());  
  }
  
  callHook(vm, 'beforeCreate');
  
  new Watcher(vm, updateComponent, ()=>{
    callHook(vm, 'created');
  },true)
  
  callHook(vm, 'mounted');
}

四,结尾

本篇,对组件相关流程与实现进行了简单总结;

【Vue2.x 源码学习】第一阶段完结,后续将持续进行优化;