【手写 Vue2.x 源码】第四十一篇 - 组件部分 - 生成组件的真实节点

264 阅读3分钟

一,前言

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

本篇,组件部分-生成组件的真实节点;


二,生成组件的真实节点

1,前文回顾

前篇,在 createElement 方法中,扩展了对组件的处理:createComponent 方法:生成组件的虚拟节点;

按照模板的渲染流程,接下来会进入 patch 方法,其中的 createElm 方法:将虚拟节点转化成为真实节点;

// todo 后续需详细描述相关流程,对 patch 和 createElm 方法进行必要的介绍和说明;

2,createElm 方法

patch 方法中,createElm 方法会将虚拟节点生成为真实节点:

通过vnode.el = document.createElement(tag)直接创建出真实节点

export function createElm(vnode) {
  // vnode.el:绑定真实节点与虚拟节点的映射关系,便于后续的节点更新操作

  // 虚拟节点必备的三个:标签,数据,孩子
  let { tag, data, children, text, vm } = vnode;
  
  if (typeof tag === 'string') { // 处理元素节点
    // 创建真实节点
    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;
} 

现在,由于组件的加入,createElm 方法中可能会存在 componentOptions

打印 createElm 查看:

第一次:真实节点:id=app

image.png

第二次:组件:my-button

image.png

添加对组件的处理

/**
 * 创造组件的真实节点
 * @param {*} vnode 
 */
function createComponent(vnode) {
  console.log(vnode) // my-button
}

export function createElm(vnode) {
  let { tag, data, children, text, vm } = vnode;
  
  if (typeof tag === 'string') {// 元素 or 组件
    // 添加对组件的处理
    if(createComponent(vnode)){ // 将组件的虚拟节点,创建成为组件的真实节点

    }
    
    // 创建真实节点
    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;
} 

3,组件的初始化 Hook

之前在组件的 data 属性上,扩展出了生命周期 hook

createComponent 中获取 hook,如果有 hook 说明就是组件;

拿到 hook 中的 init 方法,并使用 init 方法处理 vnode

/**
 * 创造组件的真实节点
 * @param {*} vnode 
 */
function createComponent(vnode) {
  console.log(vnode);
  let i = vnode.data;
  // 先确定有 hook;再拿到 init 方法;
  if((i = i.hook)&&(i = i.init)){ // 最后 i 为 init 方法
    i(vnode); // 使用 init 方法处理 vnode
  }
}

备注:

  • 先将 hook 赋值给 i:看是否有 hook,如果有 hook 就是组件;
  • 再将 hook 中的 init 方法赋值给 i;
  • 最终 i 就是 hook 上的init 方法;

使用 hook 上的 init 方法处理 vnode,在 hook 中进行组件的初始化:

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

  if(isObject(Ctor)){
    Ctor = vm.$options._base.extend(Ctor)
  }

  data.hook = {
    init(){
      // 创建组件的实例并挂载
      let child = new Ctor({});
      child.$mount();
    },
    prepatch(){},
    postpatch(){}
  }
  
  let componentVnode = vnode(vm, tag, data, undefined, key, undefined, {Ctor, children, tag});
  
  return componentVnode;
}

new Ctor 时,会执行 _init 进行组件的初始化: // 调用子类的初始化 _init 方法

  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() 进行挂载,但没有传参,此时 el = null,所以不会执行挂载:

  Vue.prototype.$mount = function (el) {
    const vm = this;
    const opts = vm.$options;
    el = document.querySelector(el); // 获取真实的元素
    vm.$el = el;// $el:页面上的真实元素

    if (!opts.render) {
      let template = opts.template;
      if (!template) {
        template = el.outerHTML;
      }

      let render = compileToFunction(template);
      opts.render = render;
    }

    mountComponent(vm);
  }

如果组件的 render 函数不存在,使用组件的 template 编译为 render 函数并保存起来,之后 mountComponent 进行组件的挂载:

export function mountComponent(vm) {

  let updateComponent = ()=>{
    vm._update(vm._render());  
  }

  callHook(vm, 'beforeCreate');
  
  // 生成渲染 watcher :每个组件都具有独立的渲染 watcher
  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
  }

通过 render.call 产生虚拟节点,返回的 vnode 就是模板的 button

todo 这一节,应该是生成虚拟节点,需要重构;


三,结尾

本篇,介绍了组件部分-生成组件的真实节点;

下篇,组件部分 - 组件挂载流程简述;


维护记录

  • 20230228:
    • 调整了文章目录结构;
    • 添加 todo:重新梳理规划组件部分知识点,重新进行拆分和优化;