写点什么

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

用户头像
Brave
关注
发布于: 3 小时前
【Vue2.x 源码学习】第四十三篇 - 组件部分 - 组件相关流程总结

一,前言


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


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


  1. 响应式数据原理(第一篇 ~ 第十篇)

  2. 模板编译原理(第十一篇 ~ 第二十篇)

  3. 依赖收集、异步更新、生命周期(第二十一篇 ~ 第二十七篇)

  4. diff 算法原理(第二十八篇 ~ 第三十三篇)

  5. 组件部分(第三十四篇 ~ 第四十二篇)


简单总结一下问题,及专栏后续的完善工作:


  • 响应式数据原理部分:写的有些乱,顺序上还需要再做调整,好在事情讲清楚了;

  • 模板编译原理部分:部分内容仍需要再细化,要把流程讲清楚,让人一看就懂;

  • diff 算法部分:还需要添加一些必要图示和动画,这部分不难,但做好不容易;

  • 组件部分:近半月工作比较忙,这部分质量并不高,二轮首先优化这块;


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


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


二,主要流程划分


  • Vue.component 的实现

  • Vue.extend 的实现

  • 组件合并的实现

  • 组件编译的实现

  • 创建组件的虚拟节点

  • 组件生命周期的实现

  • 创建组件的真实节点

  • 组件挂载的实现


三,各流程的实现简述

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.jsexport function initGlobalAPI(Vue) {  Vue.options = {};  // 便于组件通过 vm.$options._base 拿到 Vue;   Vue.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;

  • 返回组件的构造函数 Sub,Vue.component 中将对组件构造函数进行全局映射


// src/global-api/index.jsexport 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#initMixinVue.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.jslet 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.jsexport 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);}
// 创建组件虚拟节点 componentVnodefunction 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.jsfunction 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 参数 el 为 null,所以也不会进行挂载;

  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.jsexport 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.jsexport 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 方法中 oldVnode 为 null(组件的 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.jsexport 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 源码学习】第一阶段完结,后续将持续进行优化;

用户头像

Brave

关注

还未添加个人签名 2018.12.13 加入

还未添加个人简介

评论

发布
暂无评论
【Vue2.x 源码学习】第四十三篇 - 组件部分 - 组件相关流程总结