【手写 Vue2.x 源码】temp第四十篇 - 组件部分 - 组件的生命周期

262 阅读5分钟

一,前言

上篇,介绍了组件部分-组件的编译,主要涉及以下几部分:

  • 组件编译流程介绍:html->render->vnode
  • 扩展createElement,创建组件虚拟节点:createComponent

本篇,组件部分-组件的生命周期;


二,前文回顾

  1. Vue.component 方法:
  • Vue.component 方法,用于 Vue 全局组件的声明;
  • 当组件定义 definition 为对象时,在 Vue.component 内部会使用 Vue.extend 方法对组件定义 definition 进行处理,返回组件的构造函数;
  • 将组件与对应的构造函数的映射关系维护到全局对象 Vue.options.components 中,便于后续的使用;
  1. 组件初始化时:
  • new Vue 会执行 Vue 的原型方法 _init 进行初始化流程,在 mergeOptions 中会进行组件的合并;
  • 通过 mergeOptions(vm.constructor.options, options) 将全局组件定义合并到局部组件定义上;
  • 组件查找时,会优先查找对应的局部组件定义,若未找到再通过链继续向上查找全局组件定义(组件.__proto__ = 全局组件);
  1. 组件的编译流程:
  • 与模板编译流程一致:html 模板 -> AST语法树 -> 生成 render 函数,执行 _c 创建虚拟节点;(_ccreateElement 方法);
  • createElement 方法中,如果是组件,则通过 createcreateComponentComponent 方法,创建组件的虚拟节点componentVnode
  • createComponent 方法中,当 Ctor 为对象时,需要使用 Vue.extend 进行处理,生成组件的构造函数;(所以,所有的组件都是通过Vue.extend 方法来实现的)
  • 在组件的虚拟节点 componentVnode 中,包含了组件选项 componentOptionscomponentOptions 中包含组件构造函数 Ctor;(此时,componentOptions 中的 Ctor 由于已经被 Vue.extend 处理,所以一定为函数)

componentOptions 组件选项中,存放着组件的所有内容,比如:属性的实现、事件的实现、插槽的内容,Ctor 等;

到这里,就创建出了组件的虚拟节点;

接下来,根据组件的虚拟节点创建出组件的真实节点,之后再进行挂载;


三,组件的生命周期

new Ctor() 当组件初始化时 ,会执行初始化的回调函数

hook 钩子,在不同时机会执行不同的钩子函数;

通过扩展组件的 data 属性,为组件添加生命周期钩子函数:

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

  if(isObject(Ctor)){
    // 获取 Vue 并通过 Vue.extend 将对象处理成为组件的构造函数
    Ctor = vm.$options._base.extend(Ctor)
  }
  
  // 扩展组件的生命周期
  data.hook = {
    init(){
      let child = new Ctor({});
      child.$mount();
    },
    prepatch(){},
    postpatch(){}
  }
  
  // 创建 vnode 时,组件是没有文本的,需要传入 undefined
  // 注意:组件没有孩子,组件的孩子就是插槽,所以要将 children 放到组件的选项中
  let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
  
  return componentVnode;
}

4,创建组件的真实节点

问题:如何创建组件的真实节点?以标签为元素的处理流程作为参照:

  • createElement 执行完成后,接下来就走到了 patch

(注意,这里的 create,是 createElement 吗?)

  • 将虚拟节点转换为真实节点的方法是 createElm ;

由于组件的加入,此时的 createElm 方法中可能会包含 componentOptions

测试当前代码,会进入两次 debugger:

第一次:是一个 idapp 的真实节点

01.png

第二次:是一个 my-button 组件

02.png

当前 createElm 方法中,是直接通过 document.createElement(tag) 创建标签;

现在,还需要添加对组件类型的处理,通过 createComponent 创建组件的真实节点:

/**
 * 创造组件的真实节点
 * @param {*} vnode 
 */
function createComponent(vnode) {
  // 第一次进入时,是 id=app,此时 vnode 为 undefined
  console.log(vnode)
}

// 面试题:虚拟节点的实现?如何将虚拟节点渲染成真实节点
export function createElm(vnode) {

  // 虚拟节点必备的三个:标签,数据,孩子
  let { tag, data, children, text, vm } = vnode;
  debugger;
  
  // vnode.el:绑定真实节点与虚拟节点的映射关系,便于后续的节点更新操作
  if (typeof tag === 'string') { // 元素
  
    // 组件的处理
    if(createComponent(vnode)){ // 将组件的虚拟节点,创建成为组件的真实节点
        // todo... 
    }

    // 处理当前元素节点
    vnode.el = document.createElement(tag) // 创建元素的真实节点
    updateProperties(vnode, data)  // 处理元素的 data 属性
    
    // 处理当前元素节点的儿子:递归创建儿子的真实节点,并添加到对应的父亲中
    children.forEach(child => { // 若不存在儿子,children为空数组
      vnode.el.appendChild(createElm(child))
    });
    
  } else { // 文本:文本中 tag 是 undefined
    vnode.el = document.createTextNode(text)  // 创建文本的真实节点
  }
  
  return vnode.el;
}

createComponent 方法中,在 data 属性上进行组件生命周期的扩展:

创建组件的真实节点,需要与组件做一个初始化操作,之前预留的钩子 init

// src/vdom/index.js

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

  if(isObject(Ctor)){
    // 获取 Vue 并通过 Vue.extend 将对象处理成为组件的构造函数
    Ctor = vm.$options._base.extend(Ctor)
  }
  
  // 扩展组件的生命周期
  data.hook = {
    init(){},
    prepatch(){},
    postpatch(){}
  }
  
  // 创建vnode时,组件是没有文本的,需要传入 undefined
  // 注意:组件没有孩子,组件的孩子就是插槽,将 children 放到组件的选项中
  let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
  
  return componentVnode;
}

createComponent 方法中,尝试获取生命周期 hook,若 hook 存在则说明是组件,继续拿到 init 方法,处理虚拟节点 vnode

/**
 * 创造组件的真实节点
 * @param {*} vnode 
 */
function createComponent(vnode) {
  console.log(vnode);
  
  let i = vnode.data;
  if((i = i.hook)&&(i = i.init)){ // 最后 i 为 init 方法
    i(vnode); // 将 vnode 传入 init 方法
  }
}

先把 hook 赋值给 i,确定是否存在 hook;再把 init 赋值给 i,最终 i 为 init 方法;


todo: 5、6、7 部分的说明,在本节可以省略掉,暂不处理

5,创建组件的真实节点

通过 new Ctor 得到组件的实例,调用组件的 $mount 方法,生成一个 $el;

vnode.componentInstance = new Ctor()

vnode.componentInstance.$el 即为组件渲染后的结果;

6,将组件的 vnode.componentInstance.$el 插入到父标签中

7,当组件实例化时 new Ctor(),会进行组件的初始化,此时,会为组件添加一个独立的渲染过程,为每个组件生成各自的渲染 watcher;当组件更新时,只需要更新组件对应的渲染 watcher 即可;

所以,性能是非常高的,因为在组件渲染时,组件对应的属性只会收集自己的渲染 watcher


四,结尾

本篇,介绍了组件部分-组件的生命周期,主要涉及以下几部分:

  • 介绍了组件生命周期的实现;
  • 介绍了如何进行组件的初始化和挂载;

下一篇,组件部分-生成组件的真实节点


维护日志

  • 20210814:
    • 优化了“前文回顾”部分,总结前置知识点,能够更平滑过渡到当前内容;
  • 20230227:
    • 调整了目录结构;
    • 优化了大量的内容描述,使表述更加准确易懂;
    • 更新了文章摘要;