Vue3 → 初始化Component与element

393 阅读3分钟

初始化Component与element.png

初始化 Component

流程解析

外部引用方式

通过createAppmount方法进行挂载返回APP实例

import { crerateApp } from "../../lib/guide-mini-vue.esm.js" 
// import App from "./App.js"
import { App } from "./App.js"

const rootComponent = document.querySelector("#app")
crerateApp(App).mount(rootComponent)
  • 创建 App 组件
    • 直接使用 render 函数返回虚拟节点来渲染
    • 通过 h 函数进行将真实DOM转换为虚拟DOM
    • setup返回当前组件的数据
import { h } from "../../lib/guide-mini-vue.esm.js";
window.self = null;
export const App = {
  render() {
    window.self = this;
    return h(
      "div",
      {
        id: "123",
        class: ["lbxin", "lbxin-active"],
      },
      "name " + this.msg + " age:" + this.age
      //Proxy代理this.setup返回的值 代理this.$el(根实例 - root element)的值
      // [h("p",{class:'red'},'hi'),h("span",{class:"blue"},"Lbxin")]
    );
  },
  setup() {
    // 返回当前组件的数据
    return {
      msg: "Lbxin",
      age: 12,
    };
  },
};
// const HelloWorld = {
//   render() {
//     return h("div", "Lbxin " + this.msg);
//   },
//   setup() {
//     return {
//       msg: "min-vue",
//     };
//   },
// };
// export default {
//   name: "App",
//   setup() {},

//   render() {
//     return h("div", { tId: 1 }, [h("p", {}, "主页"), h(HelloWorld)]);
//   },
// };

实现主流程
  • crerateApp 返回 mount 进行实例化组件
    • mount 内部实现
      • 先将真实DOM节点转换为虚拟DOM,后续所有内部实现都是基于当前的虚拟DOM实例进行操作的
      • 参数是外部的真实DOM
      • 转换后使用虚拟DOM进行渲染
import { render } from "./render";
import { createVNode } from "./vnode";

export function crerateApp(rootComponent) {
  return {
    mount: function (rootContainer) {
      // 先转换为虚拟节点vNode,后续所有操作都是基于该虚拟节点进行操作的
      // 接受的参数是一个节点组件,所以需要先将组件转换为虚拟节点  Component → vNode
      const vNode = createVNode(rootComponent);
      //直接使用VNode虚拟节点进行渲染
      render(vNode, rootContainer);
    },
  };
}
  • h 函数 真实DOM转换为虚拟DOM
import { createVNode } from "./vnode";
export function h(type, props?, children?) {
  // 用来将DOM的描述信息转换为虚拟DOM
  return createVNode(type, props, children);
}

  • createVNode 将组件信息组合成一个对象(虚拟DOM)并返回
export function createVNode(type, props?, children?) {
  // 将组件信息组合为一个对象返回 虚拟DOM
  const vNode = {
    type,
    props,
    children,
    el: null,
  };
  return vNode;
}
  • render 调用patch方法,方便后续递归处理
export function render(vnode, container) {
  // 调用patch函数 方便进行后续递归处理
  patch(vnode, container);
}
  • patch 处理组件 组件分为封装的组件和普通DOM元素
function patch(vnode, container) {
  // 处理组件 分为普通节点和封装的组件
  if (typeof vnode.type === "string") {
    processElement(vnode, container);
  } else if (isObject(vnode.type)) {
    processComponent(vnode, container); //挂载组件
  }
}
  • processComponent 处理组件 执行转换挂载操作
function processComponent(vnode: any, container: any) {
  // 挂载组件
  mountComponent(vnode, container);
}
  • mountComponent 通过虚拟节点创建组件实例
    • 通过虚拟节点创建组件实例,并将原生传入的属性挂载到该组件实例上
    • createComponentInstance 创建组件实例,并挂载相应的属性
    function mountComponent(initialVNode: any, container) {
      // 通过虚拟节点创建组件实例
      const insatnce = createComponentInstance(initialVNode);
      const { data } = insatnce.type
    
      // 通过data函数获取原始数据,并调用reactive函数将其包装成响应式数据
      // const state = reactive(data())
    
      // 为了使得自身状态值发生变化时组件可以实现更新操作,需要将整个渲染任务放入到Effect中进行收集
      effect(() => {
        setupComponent(insatnce); //处理setup的信息 初始化props  初始化Slots等
        setupRenderEffect(insatnce, initialVNode, container); // 首次调用App组件时会执行  并将render函数的this绑定为创建的代理对象
    
      })
    }
    export function createComponentInstance(vnode) {
      const component = {
        vnode,
        type: vnode.type,
        render: vnode.render,
        setupState: {},
      };
    
      return component;
    }
    
    • setupComponent(insatnce) 处理setup的信息 初始化props 初始化Slots等
      • 通过Proxy进行代理 setup中的数据 从而使得内部的this可以访问到setup中的数据 - 拦截this
        • 当访问Proxy时 会先访问到组件实例中获取组件实例的setupState属性 即setup函数返回的值,可以通过此进行判断
      • setup可以返回函数或对象 函数-是组件的render函数 对象-将对象返回的对象注入到这个组件上下文中 setup返回当前组件的数据
      • handleSetupResult 将setup中的数据添加到实例上
      • finishComponentSetup 将组件对象上的render添加到实例对象上
export function setupComponent(instance) {
  // 处理setup的信息 初始化props  初始化Slots等
  // initProps()
  // initSlots()
  setupStatefulComponent(instance);
}

function setupStatefulComponent(instance: any) {
  // 调用组件的setup
  // 首次调用patch时 instance.type就是传入的App组件
  // const Component = instance.vNode.type
  const Component = instance.type;
  instance.proxy = new Proxy(
    { _: instance },
    PublicInstanceProxyHandlers
    // {
    //     get(target,key){
    //         const { setupState } = instance
    //         if(key in setupState){
    //             return setupState[key]
    //         }

    //         if(key === '$el'){
    //             return instance.vnode.el
    //         }
    //     }
    // }
  );
  const { setup } = Component;

  if (setup) {
    // setup可以返回函数或对象 函数-是组件的render函数 对象-将对象返回的对象注入到这个组件上下文中
    const setupResult = setup();
    // setup返回当前组件的数据
    handleSetupResult(instance, setupResult);
  }
}
function handleSetupResult(instance, setupResult: any) {
  // fuanction object
  // 将setup中的数据添加到实例上
  if (typeof setupResult === "object") {
    instance.setupState = setupResult;
  }
  finishComponentSetup(instance);
}

function finishComponentSetup(instance: any) {
  // 将组件对象上的render添加到实例对象上
  const Component = instance.type;
  if (Component.render) {
    instance.render = Component.render;
  }
}

// 在组件中通过 `this.$el` 可以访问到组件对应的真实DOM。
const publicPropertiesMap = {
  $el: (i) => i.vnode.el,
};
export const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
    const { setupState } = instance;
    if (key in setupState) {
      return setupState[key];
    }

    // if(key === '$el'){
    //     return instance.vnode.el
    // }
    const publicGetter = publicPropertiesMap[key];
    if (publicGetter) {
      return publicGetter(instance);
    }
  },
};

注意:mountElement是比setupStatefulComponent后执行,setupStatefulComponent执行的时候,vnode.el不存在,后续mountelement的时候,vnode才会有值,所以直接在mountElement中挂载$el是有问题的,需要在render函数执行后通过执行结果将el挂在到虚拟节点上,而setupRenderEffect在首次调用App组件时就会执行

function setupRenderEffect(
  // state,
  // insatnce: {
  //   vnode: any;
  //   type: any; // 调用patch函数
  //   render: any
  // },
  insatnce: any,
  initialVNode,
  container
) {
  // 根据VNode获取组件的选项对象
  // const subTree = insatnce.vnode.render.call(state,state);
  const { proxy } = insatnce
  //指定instance中的this到当前节点 统一this 而非外部变化的实例等指向
  const subTree = insatnce.render.call(proxy);
  // 通过render获取到组件需要渲染的内容 即render函数返回的虚拟DOM
  // 通过调用patch函数来挂载组件所需要描述的内容 即subTree
  patch(subTree, container);
  // 所有的 subTree 都初始化结束
  initialVNode.el = subTree.el  // 存储根节点到VNode中 方便后续获取
}
  • setupRenderEffect 将render函数的this绑定为创建的代理对象 首次渲染会默认执行
function setupRenderEffect(
  // state,
  // insatnce: {
  //   vnode: any;
  //   type: any; // 调用patch函数
  //   render: any
  // },
  insatnce: any,
  initialVNode,
  container
) {
  // 根据VNode获取组件的选项对象
  // const subTree = insatnce.vnode.render.call(state,state);
  const { proxy } = insatnce
  //指定instance中的this到当前节点 统一this 而非外部变化的实例等指向
  // App组件中的render方法中的this指的是App组件  需要改到组件实例的proxy属性
  // instance.render 来自于 finishComponentSetup 方法,就是组件的render方法
  // 让render中的this指向创建的代理对象
  const subTree = insatnce.render.call(proxy);
  // 通过render获取到组件需要渲染的内容 即render函数返回的虚拟DOM
  // 通过调用patch函数来挂载组件所需要描述的内容 即subTree
  // subTree指的就是class="root"的根节点
  patch(subTree, container);
  // 所有的 subTree 都初始化结束
  
  // 后续就可以通过this.$el访问到虚拟DOM对应的真实DOM了
  initialVNode.el = subTree.el  // 存储根节点到VNode中 方便后续获取
}

初始化 element

流程解析

内部实现
  • patch 打补丁时进行判断指定处理
function patch(vnode, container) {
  // 处理组件 分为普通节点和封装的组件
  if (typeof vnode.type === "string") {
    processElement(vnode, container);
  } else if (isObject(vnode.type)) {
    processComponent(vnode, container); //挂载组件
  }
}
  • mountElement 将虚拟DOM挂载到对应的容器上
function processElement(vnode: any, container: any) {
  mountElement(vnode, container)
}
function mountElement(vnode: any, container: any) {
  // vnode → element → div
  const { children, props, type } = vnode
  // 创建对应的DOM,同时绑定到虚拟DOM上
  // 创建真实DOM时需要将真实DOM也挂载到虚拟DOM的el上  后续进行$el拦截时需要返回真实DOM的实例
  const el = (vnode.el = document.createElement(type))

  if (typeof children === "string") {
    // 普通文本 直接赋值给textContent
    el.textContent = children
  } else if (Array.isArray(children)) {
    // 多个子元素 需要循环递归patch
    mountChildren(vnode, container)
  }
  for (const key in props) {
    if (Object.prototype.hasOwnProperty.call(props, key)) {
      const val = props[key];
      el.setAttribute(key, val)
    }
  }
  container.append(el)
}

function mountChildren(vnode: any, container: any) {
  vnode.children.forEach(element => {
    patch(element, container)
  });
}