【mini-vue】runtime-core模块学习

计光赫
2023-12-01

目录

createApp

createApp

vnode

createVNode

createTextVNode

getShapeFlag

normalizeVNode

h

h

renderer

render

patch

processText

processFragment

processElement

mountElement

mountChildren

processComponent

mountComponent

updateComponent

patchChildren

patchKeyedChildren

setupRenderEffect

updateComponentPreRender

component

createComponentInstance

setupComponent

setupStatefulComponent

handleSetupResult

finishComponentSetup(待)

currentInstance公共变量

getCurrentInstance

setCurrentInstance

componentPublicInstance

PublicInstanceProxyHandlers对象

publicPropertiesMap对象

componentProps

initProps

componentEmit

emit

componentSlots

initSlots

normalizeObjectSlots

normalizeSlotValue

renderSlots

componentRenderUtils

shouldUpdateComponent

hasPropsChanged

apiInject

provide

inject


createApp

createApp

参数:rootComponent对象,是一个JavaScript对象(可能包含name、setup、render等属性)

返回值:返回一个带有mounted方法的对象

作用:

  1. 创建一个带有mounted方法的对象
  2. mounted方法接收一个根容器对象rootContainer,在mounted方法中调用createVNode函数并传入参数rootComponent,创建虚拟节点vnode,调用render函数传入参数vnode和rootContainer
export function createAppAPI(render) {
  return function createApp(rootComponent) {
    const app = {
      _component: rootComponent,
      mount(rootContainer) {
        console.log("基于根组件创建 vnode");
        const vnode = createVNode(rootComponent);
        console.log("调用 render,基于 vnode 进行开箱");
        render(vnode, rootContainer);
      },
    };

    return app;
  };
}

vnode

createVNode

参数:

  1. type,可以是对象或者字符串,如果是对象则代表组件对象,字符串代表普通元素
  2. props,一个对象,包含attribute和各种事件
  3. children,可以是数组或者字符串,如果是数组则代表子组件,字符串代表textContent文本

返回值:vnode虚拟节点对象

作用:

  1. 创建虚拟节点vnode对象,包含type、props、children、component(若该节点是组件对象则有值,详见createComponentInstance方法)、key(方便DIFF算法)、shapeFlag(一种用二进制代表vnode类型的标识)、el(真实的dom节点,初始化为null)
  2. getShapeFlag方法传入type来判断这个虚拟节点是元素还是组件对象
  3. 根据children判断这个虚拟节点的children是string(文本)还是array(数组)或者是object(插槽)
  4. 返回这个vnode
export const createVNode = function (
  type: any,
  props?: any,
  children?: string | Array<any>
) {
  // 注意 type 有可能是 string 也有可能是对象
  // 如果是对象的话,那么就是用户设置的 options
  // type 为 string 的时候
  // createVNode("div")
  // type 为组件对象的时候
  // createVNode(App)
  const vnode = {
    el: null,
    component: null,
    key: props?.key,
    type,
    props: props || {},
    children,
    shapeFlag: getShapeFlag(type),
  };

  // 基于 children 再次设置 shapeFlag
  if (Array.isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  } else if (typeof children === "string") {
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
  }

  normalizeChildren(vnode, children);

  return vnode;
};

createTextVNode

参数:text字符串

返回值:返回createVNode(Text, {}, text)一个文本vnode

作用:调用createVNode(Text, {}, text)返回一个文本vnode

export const Text = Symbol("Text");

export function createTextVNode(text: string = " ") {
  return createVNode(Text, {}, text);
}

getShapeFlag

参数:type

返回值:element标识或者component标识

作用:根据type判断虚拟节点是element还是component并返回对应标识

// 基于 type 来判断是什么类型的组件
function getShapeFlag(type: any) {
  return typeof type === "string"
    ? ShapeFlags.ELEMENT
    : ShapeFlags.STATEFUL_COMPONENT;
}

normalizeVNode

参数:child,子节点

返回值:虚拟节点

作用:

  1. 若child为字符串或数字,则创建Text文本节点

  2. 若是其他节点则直接返回

h

h

参数:

  1. type
  2. props
  3. children

返回值:调用createVNode返回虚拟节点

作用:调用createVNode返回虚拟节点

export const h = (type: any , props: any = null, children: string | Array<any> = []) => {
  return createVNode(type, props, children);
};

renderer

render

参数:

  1. vnode,虚拟节点
  2. container,容器,一个dom节点

返回值:无

作用:调用patch函数

  const render = (vnode, container) => {
    console.log("调用 path")
    patch(null, vnode, container);
  };

patch

参数:

  1. n1,老的虚拟节点
  2. n2,新的虚拟节点
  3. container,容器,dom节点,当虚拟节点生成真实节点后将其放置在容器内
  4. parentComponent,父节点的虚拟节点
  5. anchor,锚点,保存一个真实dom节点,便于调用insertBefore函数将生成的el插入到这个dom节点前

返回值:无

作用:

  1. 根据type和shapeFlag进行判断,如果是Fragment则执行processFragment
  2. 如果是Text则执行processText
  3. 如果是Element则执行processElement
  4. 如果是component则执行processComponent
  function patch(
    n1,
    n2,
    container = null,
    anchor = null,
    parentComponent = null
  ) {
    // 基于 n2 的类型来判断
    // 因为 n2 是新的 vnode
    const { type, shapeFlag } = n2;
    switch (type) {
      case Text:
        processText(n1, n2, container);
        break;
      // 其中还有几个类型比如: static fragment comment
      case Fragment:
        processFragment(n1, n2, container);
        break;
      default:
        // 这里就基于 shapeFlag 来处理
        if (shapeFlag & ShapeFlags.ELEMENT) {
          console.log("处理 element");
          processElement(n1, n2, container, anchor, parentComponent);
        } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
          console.log("处理 component");
          processComponent(n1, n2, container, parentComponent);
        }
    }
  }

processText

参数:

  1. n1,老的虚拟节点
  2. n2,新的虚拟节点
  3. container,容器,dom节点

返回值:无

作用:处理文本节点

  1. 从vnode对象中取出children(这里的children是一个string,文本)
  2. 调用document.createTextNode传入这个children创建dom文本节点
  3. 将这个dom文本节点绑定到vnode的el上
  4. 在容器container中添加上这个dom文本节点
  function processText(n1, n2, container) {
    console.log("处理 Text 节点");
    if (n1 === null) {
      // n1 是 null 说明是 init 的阶段
      // 基于 createText 创建出 text 节点,然后使用 insert 添加到 el 内
      console.log("初始化 Text 类型的节点");
      hostInsert((n2.el = hostCreateText(n2.children as string)), container);
    } else {
      // update
      // 先对比一下 updated 之后的内容是否和之前的不一样
      // 在不一样的时候才需要 update text
      // 这里抽离出来的接口是 setText
      // 注意,这里一定要记得把 n1.el 赋值给 n2.el, 不然后续是找不到值的
      const el = (n2.el = n1.el!);
      if (n2.children !== n1.children) {
        console.log("更新 Text 类型的节点");
        hostSetText(el, n2.children as string);
      }
    }
  }

processFragment

参数:

  1. n1,老的虚拟节点
  2. n2,新的虚拟节点
  3. container,容器,dom节点
  4. parentComponent,父节点的虚拟节点
  5. anchor,锚点

返回值:无

作用:处理Fragment节点,即vue的template类似效果和React.Fragment差不多的效果

调用mountChildren挂载n2的子节点

  function processFragment(n1: any, n2: any, container: any) {
    // 只需要渲染 children ,然后给添加到 container 内
    if (!n1) {
      // 初始化 Fragment 逻辑点
      console.log("初始化 Fragment 类型的节点");
      mountChildren(n2.children, container);
    }
  }

processElement

参数:

  1. n1,老的虚拟节点
  2. n2,新的虚拟节点
  3. container,容器,dom节点
  4. parentComponent,父节点的虚拟节点
  5. anchor,锚点

返回值:无

作用:处理element节点

  1. 如果n1为空,调用mountElement
  2. 如果n1有值,则调用patchElement
  function processElement(n1, n2, container, anchor, parentComponent) {
    if (!n1) {
      mountElement(n2, container, anchor);
    } else {
      // todo
      updateElement(n1, n2, container, anchor, parentComponent);
    }
  }

mountElement

参数:

  1. vnode,新的虚拟节点
  2. container,容器,dom节点
  3. parentComponent,父节点的虚拟节点
  4. anchor,锚点

返回值:无

作用:挂载element节点

  1. 调用hostCreateElement方法,dom平台即document.createElement方法传入vnode.type创建标签名称为vnode.type的html元素
  2. 将这个html元素绑定到vnode.el
  3. 从vnode中取出children和shapeFlag,如果是文本节点则vnode.el.textContent = children(文本节点),如果是数组节点则调用mountChildren挂载子组件
  4. 从vnode中取出props,遍历props,调用hostPatchProp方法,dom平台即,如果是on开头的属性则进行事件监听addEventListener,如果不是则设置属性setAttribute
  5. 调用hostInsert,dom平台即调用insertBefore方法,在容器中添加这个vnode.el
  function mountElement(vnode, container, anchor) {
    const { shapeFlag, props } = vnode;
    // 1. 先创建 element
    // 基于可扩展的渲染 api
    const el = (vnode.el = hostCreateElement(vnode.type));

    // 支持单子组件和多子组件的创建
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // 举个栗子
      // render(){
      //     return h("div",{},"test")
      // }
      // 这里 children 就是 test ,只需要渲染一下就完事了
      console.log(`处理文本:${vnode.children}`);
      hostSetElementText(el, vnode.children);
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 举个栗子
      // render(){
      // Hello 是个 component
      //     return h("div",{},[h("p"),h(Hello)])
      // }
      // 这里 children 就是个数组了,就需要依次调用 patch 递归来处理
      mountChildren(vnode.children, el);
    }

    // 处理 props
    if (props) {
      for (const key in props) {
        // todo
        // 需要过滤掉vue自身用的key
        // 比如生命周期相关的 key: beforeMount、mounted
        const nextVal = props[key];
        hostPatchProp(el, key, null, nextVal);
      }
    }

    // todo
    // 触发 beforeMount() 钩子
    console.log("vnodeHook  -> onVnodeBeforeMount");
    console.log("DirectiveHook  -> beforeMount");
    console.log("transition  -> beforeEnter");

    // 插入
    hostInsert(el, container, anchor);

    // todo
    // 触发 mounted() 钩子
    console.log("vnodeHook  -> onVnodeMounted");
    console.log("DirectiveHook  -> mounted");
    console.log("transition  -> enter");
  }

mountChildren

参数:

  1. vnode,虚拟节点
  2. container,容器,dom节点
  3. parentComponent,父组件的虚拟节点

返回值:无

作用:遍历vnode.children对每一个child调用patch挂载到container中

  function mountChildren(children, container) {
    children.forEach((VNodeChild) => {
      // todo
      // 这里应该需要处理一下 vnodeChild
      // 因为有可能不是 vnode 类型
      console.log("mountChildren:", VNodeChild);
      patch(null, VNodeChild, container);
    });
  }

processComponent

参数:

  1. vnode,虚拟节点
  2. container,容器,dom节点
  3. parentComponent,父组件的?????????

返回值:无

作用:

  1. 如果n1为空,调用mountComponent挂载组件

  2. 如果n1有值,则调用updateComponent更新组件

  function processComponent(n1, n2, container, parentComponent) {
    // 如果 n1 没有值的话,那么就是 mount
    if (!n1) {
      // 初始化 component
      mountComponent(n2, container, parentComponent);
    } else {
      updateComponent(n1, n2, container);
    }
  }

mountComponent

参数:

  1. initialVNode,虚拟节点
  2. container,容器,dom节点
  3. parentComponent,父组件的虚拟节点
  4. anchor,锚点

返回值:无

作用:

  1. 调用createComponentInstance,生成组件实例instance
  2. 调用setupComponent
  3. 调用setupRenderEffect
  function mountComponent(initialVNode, container, parentComponent) {
    // 1. 先创建一个 component instance
    const instance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent
    ));
    console.log(`创建组件实例:${instance.type.name}`);
    // 2. 给 instance 加工加工
    setupComponent(instance);

    setupRenderEffect(instance, initialVNode, container);
  }

updateComponent

参数:

  1. n1,老的虚拟节点
  2. n2,新的虚拟节点

返回值:无

作用:

  1. 将老的component赋值给新的component
  2. 调用shouldUpdateComponent方法判断组件是否需要更新
  3. 若需要更新,则将n2赋值给新的component,调用n2.component的update方法更新组件
  4. 若不需要更新,则将n1的el赋值给n2的el,将n2赋值给n2.component的vnode
  // 组件的更新
  function updateComponent(n1, n2, container) {
    console.log("更新组件", n1, n2);
    // 更新组件实例引用
    const instance = (n2.component = n1.component);
    // 先看看这个组件是否应该更新
    if (shouldUpdateComponent(n1, n2)) {
      console.log(`组件需要更新: ${instance}`);
      // 那么 next 就是新的 vnode 了(也就是 n2)
      instance.next = n2;
      // 这里的 update 是在 setupRenderEffect 里面初始化的,update 函数除了当内部的响应式对象发生改变的时候会调用
      // 还可以直接主动的调用(这是属于 effect 的特性)
      // 调用 update 再次更新调用 patch 逻辑
      // 在update 中调用的 next 就变成了 n2了
      // ps:可以详细的看看 update 中 next 的应用
      // TODO 需要在 update 中处理支持 next 的逻辑
      instance.update();
    } else {
      console.log(`组件不需要更新: ${instance}`);
      // 不需要更新的话,那么只需要覆盖下面的属性即可
      n2.component = n1.component;
      n2.el = n1.el;
      instance.vnode = n2;
    }
  }

patchChildren

参数:

  1. n1,老的虚拟节点
  2. n2,新的虚拟节点
  3. container,容器,dom节点
  4. parentComponent,父节点的虚拟节点
  5. anchor,锚点

返回值:无

作用:

根据新老虚拟节点的shapeFlag来判断该如何进行处理

  1. 新的children为文本,老的为数组,则先卸载老的数组,挂载新的文本
  2. 新的children为文本,老的为文本,新老children不一致,则挂载新的文本
  3. 新的children是数组,老的是文本,删除老的文本,挂载新的数组
  4. 新的children是数组,老的是数组,调用patchKeyedChildren方法进行DIFF
  function patchChildren(n1, n2, container, anchor, parentComponent) {
    const { shapeFlag: prevShapeFlag, children: c1 } = n1;
    const { shapeFlag, children: c2 } = n2;

    // 如果 n2 的 children 是 text 类型的话
    // 就看看和之前的 n1 的 children 是不是一样的
    // 如果不一样的话直接重新设置一下 text 即可
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      if (c2 !== c1) {
        console.log("类型为 text_children, 当前需要更新");
        hostSetElementText(container, c2 as string);
      }
    } else {
      // 如果之前是 array_children
      // 现在还是 array_children 的话
      // 那么我们就需要对比两个 children 啦
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          patchKeyedChildren(c1, c2, container, anchor, parentComponent);
        }
      }
    }
  }

patchKeyedChildren

详见【mini-vue】DIFF算法学习笔记_名字太长不好不好的博客-CSDN博客

setupRenderEffect

参数:

  1. instance,组件实例
  2. initialVNode,初始的虚拟节点
  3. container,容器,dom节点

返回值:无

作用:

  1. 调用effect函数,第一个参数是一个函数,在这个函数中我们做以下操作
    1. 根据实例instance的isMounted属性判断组件是否已经挂载过,如果未挂载则执行初始化的逻辑
      1. 从实例instance中获取proxy代理对象
      2. 调用instance.render并将this绑定为proxy,生成subTree(Vue页面中的template实际上会自动转化成render函数,render函数中调用h函数即createVNode会生成虚拟节点,这个subTree就是整个虚拟组件树)
      3. 调用patch方法传入subTree、container、instance(既然我们这么费力获取到了虚拟组件树当然要patch这个虚拟节点生成真实dom并挂载到container上)
      4. initialVNode.el = subTree.el(所有的虚拟节点咱们都需要给它绑定上el,如果不执行这一步操作到时候在调用this.$el无法获取到真实的dom)
      5. instance.isMounted = true代表实例已经挂在过了
    2. 挂载过则执行更新逻辑
      1. 从实例中获取next和vnode
      2. 若next有值,将vnode的el赋值给next的el(next存储的是新组件虚拟节点),调用updateComponentPreRender函数
      3. 从instance实例中取出proxy
      4. 调用instance.render并将this绑定为proxy,生成新的subTree虚拟组件树
      5. 从instance中获取老的虚拟组件树
      6. 调用patch,传入老的和新的虚拟组件树
  2. effect的第二个函数是一个对象,对象有一个属性scheduler,值为一个函数,调用queueJobs事件队列,
  3. effect函数的返回值为,通过ReactiveEffect类创建的_effect对象中的run方法,将 这个返回值赋值给instance.update,方便后续直接通过instance.update()执行更新逻辑

备注:effect函数的理解可以详见【mini-vue】Reactive模块学习笔记_名字太长不好不好的博客-CSDN博客

  function setupRenderEffect(instance, initialVNode, container) {
    // 调用 render
    // 应该传入 ctx 也就是 proxy
    // ctx 可以选择暴露给用户的 api
    // 源代码里面是调用的 renderComponentRoot 函数
    // 这里为了简化直接调用 render

    // obj.name  = "111"
    // obj.name = "2222"
    // 从哪里做一些事
    // 收集数据改变之后要做的事 (函数)
    // 依赖收集   effect 函数
    // 触发依赖
    function componentUpdateFn() {
      if (!instance.isMounted) {
        // 组件初始化的时候会执行这里
        // 为什么要在这里调用 render 函数呢
        // 是因为在 effect 内调用 render 才能触发依赖收集
        // 等到后面响应式的值变更后会再次触发这个函数
        console.log(`${instance.type.name}:调用 render,获取 subTree`);
        const proxyToUse = instance.proxy;
        // 可在 render 函数中通过 this 来使用 proxy
        const subTree = (instance.subTree = normalizeVNode(
          instance.render.call(proxyToUse, proxyToUse)
        ));
        console.log("subTree", subTree);

        // todo
        console.log(`${instance.type.name}:触发 beforeMount hook`);
        console.log(`${instance.type.name}:触发 onVnodeBeforeMount hook`);

        // 这里基于 subTree 再次调用 patch
        // 基于 render 返回的 vnode ,再次进行渲染
        // 这里我把这个行为隐喻成开箱
        // 一个组件就是一个箱子
        // 里面有可能是 element (也就是可以直接渲染的)
        // 也有可能还是 component
        // 这里就是递归的开箱
        // 而 subTree 就是当前的这个箱子(组件)装的东西
        // 箱子(组件)只是个概念,它实际是不需要渲染的
        // 要渲染的是箱子里面的 subTree
        patch(null, subTree, container, null, instance);
        // 把 root element 赋值给 组件的vnode.el ,为后续调用 $el 的时候获取值
        initialVNode.el = subTree.el;

        console.log(`${instance.type.name}:触发 mounted hook`);
        instance.isMounted = true;
      } else {
        // 响应式的值变更后会从这里执行逻辑
        // 主要就是拿到新的 vnode ,然后和之前的 vnode 进行对比
        console.log(`${instance.type.name}:调用更新逻辑`);
        // 拿到最新的 subTree
        const { next, vnode } = instance;

        // 如果有 next 的话, 说明需要更新组件的数据(props,slots 等)
        // 先更新组件的数据,然后更新完成后,在继续对比当前组件的子元素
        if (next) {
          // 问题是 next 和 vnode 的区别是什么
          next.el = vnode.el;
          updateComponentPreRender(instance, next);
        }

        const proxyToUse = instance.proxy;
        const nextTree = normalizeVNode(
          instance.render.call(proxyToUse, proxyToUse)
        );
        // 替换之前的 subTree
        const prevTree = instance.subTree;
        instance.subTree = nextTree;

        // 触发 beforeUpdated hook
        console.log(`${instance.type.name}:触发 beforeUpdated hook`);
        console.log(`${instance.type.name}:触发 onVnodeBeforeUpdate hook`);

        // 用旧的 vnode 和新的 vnode 交给 patch 来处理
        patch(prevTree, nextTree, prevTree.el, null, instance);

        // 触发 updated hook
        console.log(`${instance.type.name}:触发 updated hook`);
        console.log(`${instance.type.name}:触发 onVnodeUpdated hook`);
      }
    }

    // 在 vue3.2 版本里面是使用的 new ReactiveEffect
    // 至于为什么不直接用 effect ,是因为需要一个 scope  参数来收集所有的 effect
    // 而 effect 这个函数是对外的 api ,是不可以轻易改变参数的,所以会使用  new ReactiveEffect
    // 因为 ReactiveEffect 是内部对象,加一个参数是无所谓的
    // 后面如果要实现 scope 的逻辑的时候 需要改过来
    // 现在就先算了
    instance.update = effect(componentUpdateFn, {
      scheduler: () => {
        // 把 effect 推到微任务的时候在执行
        // queueJob(effect);
        queueJob(instance.update);
      },
    });
  }

updateComponentPreRender

参数:

  1. instance,组件实例

  2. nextVNode,虚拟节点

返回值:无

作用:

  1. 将新的虚拟节点赋值给instance.vnode

  2. 设置instance.next为null

  3. 把新的虚拟节点的props赋值给Instance.props

  function updateComponentPreRender(instance, nextVNode) {
    // 更新 nextVNode 的组件实例
    // 现在 instance.vnode 是组件实例更新前的
    // 所以之前的 props 就是基于 instance.vnode.props 来获取
    // 接着需要更新 vnode ,方便下一次更新的时候获取到正确的值
    nextVNode.component = instance;
    // TODO 后面更新 props 的时候需要对比
    // const prevProps = instance.vnode.props;
    instance.vnode = nextVNode;
    instance.next = null;

    const { props } = nextVNode;
    console.log("更新组件的 props", props);
    instance.props = props;
    console.log("更新组件的 slots");
    // TODO 更新组件的 slots
    // 需要重置 vnode
  }

component

createComponentInstance

参数:

  1. vnode,虚拟节点
  2. parent,

返回值:虚拟组件节点(vnode、type、setupState、props、slots、provides、parent、isMounted、subTree、emit、proxy)

作用:

  1. 创建component对象,包含vnode(形参中传来的),type(vnode.type),setupState(初始化为{},保存setup内创建的响应式变量),props(初始化为{},保存父组件中传过来的值,和虚拟节点vnode中的props不同,注意区分),slots(初始化为{},保存插槽信息),provides(如果parent有值则保存为parent.provides否则初始化为{},主要是为了支持provide-inject功能),parent(形参中传来的,保存父节点的虚拟节点),isMounted(初始化false,代表组件是否被挂载过)、subTree(虚拟组件树)、emit(初始化为箭头函数,setup中的emit)、proxy(代理对象,在setupStatefulComponent中会添加上proxy,方便用户在render中调用this,render的this会被指向这个代理对象proxy,这个代理对象中包含setup中创建的响应式对象,以及$slot 、$props、$el等)
  2. 给emit绑定的第一个参数component
  3. 返回创建的component
export function createComponentInstance(vnode, parent) {
  const instance = {
    type: vnode.type,
    vnode,
    next: null, // 需要更新的 vnode,用于更新 component 类型的组件
    props: {},
    parent,
    provides: parent ? parent.provides : {}, //  获取 parent 的 provides 作为当前组件的初始化值 这样就可以继承 parent.provides 的属性了
    proxy: null,
    isMounted: false,
    attrs: {}, // 存放 attrs 的数据
    slots: {}, // 存放插槽的数据
    ctx: {}, // context 对象
    setupState: {}, // 存储 setup 的返回值
    emit: () => {},
  };

  // 在 prod 坏境下的 ctx 只是下面简单的结构
  // 在 dev 环境下会更复杂
  instance.ctx = {
    _: instance,
  };

  // 赋值 emit
  // 这里使用 bind 把 instance 进行绑定
  // 后面用户使用的时候只需要给 event 和参数即可
  instance.emit = emit.bind(null, instance) as any;

  return instance;
}

setupComponent

参数:instance实例

返回值:无

作用:初始化设置组件

  1. 调用initProps,初始化props
  2. 调用initSlots,初始化slots
  3. 调用setupStatefulComponent,初始化有状态的组件
export function setupComponent(instance) {
  // 1. 处理 props
  // 取出存在 vnode 里面的 props
  const { props, children } = instance.vnode;
  initProps(instance, props);
  // 2. 处理 slots
  initSlots(instance, children);

  // 源码里面有两种类型的 component
  // 一种是基于 options 创建的
  // 还有一种是 function 的
  // 这里处理的是 options 创建的
  // 叫做 stateful 类型
  setupStatefulComponent(instance);
}

setupStatefulComponent

参数:instance实例

返回值:无

作用:初始化有状态的组件

  1. 从instance.type中获取到组件Component(例如createApp中传入的对象,可能包含name、setup、render)
  2. 在这个component中加上一个代理对象proxy,这个代理对象主要作用是在render中调用this能够获取到正确的值,既能获取到setup中创建的响应式对象,也能获取到el,slots、props等
  3. 从组件Component中取出setup方法(这个setup来自vnode中,当然这个vnode中可能没有setup方法)
  4. 当setup方法存在(执行这个setup方法的时候,可以利用一个公共变量currentInstance保存当前实例对象instance,便于后续provide-inject逻辑的实现)
    1. 调用setCurrentInstance方法,将一个公共变量currentInstance赋值为当前instance
    2. 执行setup方法并传入instance.props(props应该用shallowReadonly包裹,是单向数据流不能轻易改变),传入context对象,即{ emit },并将结果保存在变量setupResult中
    3. 调用setCurrentInstance方法,将公共变量currentInstance重新赋值为null
    4. 调用handleSetupResult方法处理setupResult
function setupStatefulComponent(instance) {
  // todo
  // 1. 先创建代理 proxy
  console.log("创建 proxy");

  // proxy 对象其实是代理了 instance.ctx 对象
  // 我们在使用的时候需要使用 instance.proxy 对象
  // 因为 instance.ctx 在 prod 和 dev 坏境下是不同的
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
  // 用户声明的对象就是 instance.type
  // const Component = {setup(),render()} ....
  const Component = instance.type;
  // 2. 调用 setup

  // 调用 setup 的时候传入 props
  const { setup } = Component;
  if (setup) {
    // 设置当前 currentInstance 的值
    // 必须要在调用 setup 之前
    setCurrentInstance(instance);

    const setupContext = createSetupContext(instance);
    // 真实的处理场景里面应该是只在 dev 环境才会把 props 设置为只读的
    const setupResult =
      setup && setup(shallowReadonly(instance.props), setupContext);

    setCurrentInstance(null);

    // 3. 处理 setupResult
    handleSetupResult(instance, setupResult);
  } else {
    finishComponentSetup(instance);
  }
}

handleSetupResult

参数:

  1. instance,组件实例
  2. setupResult,setup函数执行的返回值

返回值:无

作用

  1. 对返回结果进行判断,如果为对象,即我们在setup中return出对象,则instance.setupState=setupResult,从而方便我们在render中通过this去使用响应式变量的时候通过代理对象instance.proxy,劫持get,去setupState中查找我们需要的响应式变量
  2. setup的返回值其实还可能为一个渲染函数,可以自定义渲染内容,用得不多,mini-vue中暂未实现
  3. 调用finishComponentSetup方法
function handleSetupResult(instance, setupResult) {
  // setup 返回值不一样的话,会有不同的处理
  // 1. 看看 setupResult 是个什么
  if (typeof setupResult === "function") {
    // 如果返回的是 function 的话,那么绑定到 render 上
    // 认为是 render 逻辑
    // setup(){ return ()=>(h("div")) }
    instance.render = setupResult;
  } else if (typeof setupResult === "object") {
    // 返回的是一个对象的话
    // 先存到 setupState 上
    // 先使用 @vue/reactivity 里面的 proxyRefs
    // 后面我们自己构建
    // proxyRefs 的作用就是把 setupResult 对象做一层代理
    // 方便用户直接访问 ref 类型的值
    // 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value
    // 这里也就是官网里面说到的自动结构 Ref 类型
    instance.setupState = proxyRefs(setupResult);
  }

  finishComponentSetup(instance);
}

finishComponentSetup(待)

参数:instance实例

返回值:无

作用:

  1. instance.render = instance.type.render
  2. compiler那块我还没去了解
function finishComponentSetup(instance) {
  // 给 instance 设置 render

  // 先取到用户设置的 component options
  const Component = instance.type;

  if (!instance.render) {
    // 如果 compile 有值 并且当组件没有 render 函数,那么就需要把 template 编译成 render 函数
    if (compile && !Component.render) {
      if (Component.template) {
        // 这里就是 runtime 模块和 compile 模块结合点
        const template = Component.template;
        Component.render = compile(template);
      }
    }

    instance.render = Component.render;
  }

  // applyOptions()
}

currentInstance公共变量

作用:保存当前的instance

主要服务于apiInject模块,使得子组件在setup中调用inject时能够直接获取到当前组件实例instance,通过instance找到parent,获取parent中的provide的值,从而实现provide-inject功能

let currentInstance = {};

getCurrentInstance

参数:无

返回值:currentInstance当前组件实例对象(只能在setup中使用)

作用:返回当前实例

// 这个接口暴露给用户,用户可以在 setup 中获取组件实例 instance
export function getCurrentInstance(): any {
  return currentInstance;
}

setCurrentInstance

参数:instance实例

返回值:无

作用:currentInstance = instance

  1. 会在每个组件实例执行setup之前调用setupCurrentInstance(instance),之后调用setupCurrentInstance(null)
  2. 这个函数起到中间层作用,方便后续维护,只要在这个函数这里打上断点方便debug
export function setCurrentInstance(instance) {
  currentInstance = instance;
}

componentPublicInstance

PublicInstanceProxyHandlers对象

作用:

  1. 对象内返回一个get方法,这个get接收 new Proxy({ _: instance }, PublicInstanceProxyHandlers) 中的instance
  2. 从instance中取出setupState和props
  3. 判断setupState中是否有key,如果有则返回对应的value
  4. 判断props中是否有key,如果有则返回对应的value
  5. 判断publicPropertiesMap中是否有key,如果有返回对应的value
// todo 需要让用户可以直接在 render 函数内直接使用 this 来触发 proxy
export const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
    // 用户访问 proxy[key]
    // 这里就匹配一下看看是否有对应的 function
    // 有的话就直接调用这个 function
    const { setupState, props } = instance;
    console.log(`触发 proxy hook , key -> : ${key}`);

    if (key[0] !== "$") {
      // 说明不是访问 public api
      // 先检测访问的 key 是否存在于 setupState 中, 是的话直接返回
      if (hasOwn(setupState, key)) {
        return setupState[key];
      } else if (hasOwn(props, key)) {
        // 看看 key 是不是在 props 中
        // 代理是可以访问到 props 中的 key 的
        return props[key];
      }
    }

    const publicGetter = publicPropertiesMap[key];

    if (publicGetter) {
      return publicGetter(instance);
    }
  },

  set({ _: instance }, key, value) {
    const { setupState } = instance;

    if (setupState !== {} && hasOwn(setupState, key)) {
      // 有的话 那么就直接赋值
      setupState[key] = value;
    }

    return true
  },
};

publicPropertiesMap对象

作用:返回$el $slots $props

const publicPropertiesMap = {
  // 当用户调用 instance.proxy.$emit 时就会触发这个函数
  // i 就是 instance 的缩写 也就是组件实例对象
  $el: (i) => i.vnode.el,
  $emit: (i) => i.emit,
  $slots: (i) => i.slots,
  $props: (i) => i.props,
};

componentProps

initProps

参数:

  1. instance
  2. rawProps

返回值:无

作用:instance.props = rawProps || {},初始化props

export function initProps(instance, rawProps) {
  console.log("initProps");

  // TODO
  // 应该还有 attrs 的概念
  // attrs
  // 如果组件声明了 props 的话,那么才可以进入 props 属性内
  // 不然的话是需要存储在 attrs 内
  // 这里暂时直接赋值给 instance.props 即可
  instance.props = rawProps;
}

componentEmit

emit

参数:

  1. instance,实例
  2. event,事件名称
  3. ...rawArgs,这个事件所需要的参数

返回值:无

作用:

  1. 从instance中获取到props
  2. 处理事件名称,生成对应的事件名handlerName,例如set-name -> onSetName
  3. 根据事件名从props中获取到对应的handler
  4. 如果存在handler则 handler(...rawArgs)
export function emit(instance, event: string, ...rawArgs) {
  // 1. emit 是基于 props 里面的 onXXX 的函数来进行匹配的
  // 所以我们先从 props 中看看是否有对应的 event handler
  const props = instance.props;
  // ex: event -> click 那么这里取的就是 onClick
  // 让事情变的复杂一点如果是烤肉串命名的话,需要转换成  change-page -> changePage
  // 需要得到事件名称
  let handler = props[toHandlerKey(camelize(event))];

  // 如果上面没有匹配的话 那么在检测一下 event 是不是 kebab-case 类型
  if (!handler) {
    handler = props[(toHandlerKey(hyphenate(event)))]
  }


  if (handler) {
    handler(...rawArgs);
  }
}

componentSlots

initSlots

参数:

  1. instance,实例
  2. children,子节点

返回值:无

作用:

  1. 从instance中获取vnode
  2. 判断如果该vnode中存在插槽,则执行normalizeObjectSlots
export function initSlots(instance, children) {
  const { vnode } = instance;

  console.log("初始化 slots");

  if (vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
    normalizeObjectSlots(children, (instance.slots = {}));
  }
}

normalizeObjectSlots

参数:

  1. children,子节点(如果该节点有插槽,则子节点的类型不是数组或字符串,而是对象)
  2. slots,插槽,是初始化后的空对象

返回值:无

作用:

  1. 遍历children获取到插槽的名称key(对具名插槽的支持)

  2. 通过children[key]获取value(value是一个函数)

  3. 创建一个新的函数,形参props是一个对象或者为空,代表value函数的形参,用于插槽传参(对作用域插槽的支持),函数体的内容调用了normalizeSlotValue函数,主要把函数value的返回结果变为一个数组

备注:配合实例代码比较好理解

    const foo = h(
      Foo,
      {},
      {
        header: ({ age }) => [
          h("p", {}, "header" + age),
          createTextVNode("你好呀"),
        ],
        footer: () => h("p", {}, "footer"),
      }
    );
    // 调用传参{age}
    return h("div", {}, [
      renderSlots(this.$slots, "header", {
        age,
      }),
      foo,
      renderSlots(this.$slots, "footer"),
    ]);
const normalizeObjectSlots = (rawSlots, slots) => {
  for (const key in rawSlots) {
    const value = rawSlots[key];
    if (typeof value === "function") {
      // 把这个函数给到slots 对象上存起来
      // 后续在 renderSlots 中调用
      // TODO 这里没有对 value 做 normalize,
      // 默认 slots 返回的就是一个 vnode 对象
      slots[key] = (props) => normalizeSlotValue(value(props));
    }
  }
};

 

normalizeSlotValue

参数:

  1. value

返回值:数组

作用:value若不为数组,则返回一个包裹着value的数组

const normalizeSlotValue = (value) => {
  // 把 function 返回的值转换成 array ,这样 slot 就可以支持多个元素了
  return Array.isArray(value) ? value : [value];
};

renderSlots

参数:

  1. slots,插槽对象,存储所有插槽

  2. name,插槽名称(具名插槽)

  3. props,插槽传参(作用域插槽)

返回值:如果插槽存在则渲染对应的真实dom节点

作用:

  1. 根据name从slots中取出对应插槽slot

  2. 若slot有值并且slot为一个函数(mini-vue这里暂时只支持对函数形式,理解了函数,其他形式都很好理解了),调用createVNode创建真实dom节点

export function renderSlot(slots, name: string, props = {}) {
  const slot = slots[name];
  console.log(`渲染插槽 slot -> ${name}`);
  if (slot) {
    // 因为 slot 是一个返回 vnode 的函数,我们只需要把这个结果返回出去即可
    // slot 就是一个函数,所以就可以把当前组件的一些数据给传出去,这个就是作用域插槽
    // 参数就是 props
    const slotContent = slot(props);
    return createVNode(Fragment, {}, slotContent);
  }
}

componentRenderUtils

shouldUpdateComponent

参数:

  1. prevVNode,旧的虚拟节点

  2. nextVNode,新的虚拟节点

返回值:布尔值,代表是否要更新组件

作用:

  1. 从prevVNode和nextVNode中取出对应props

  2. 如果新旧节点相同则不需要更新

  3. 如果没有老的虚拟节点,若新虚拟节点存在则更新,不存在则不更新

  4. 如果老的虚拟节点有值,但是新的虚拟节点没值,则要更新

  5. 调用hasPropsChanged函数根据props是否改变来判断是否需要更新

export function shouldUpdateComponent(prevVNode, nextVNode) {
  const { props: prevProps } = prevVNode;
  const { props: nextProps } = nextVNode;
  //   const emits = component!.emitsOptions;

  // 这里主要是检测组件的 props
  // 核心:只要是 props 发生改变了,那么这个 component 就需要更新

  // 1. props 没有变化,那么不需要更新
  if (prevProps === nextProps) {
    return false;
  }
  // 如果之前没有 props,那么就需要看看现在有没有 props 了
  // 所以这里基于 nextProps 的值来决定是否更新
  if (!prevProps) {
    return !!nextProps;
  }
  // 之前有值,现在没值,那么肯定需要更新
  if (!nextProps) {
    return true;
  }

  // 以上都是比较明显的可以知道 props 是否是变化的
  // 在 hasPropsChanged 会做更细致的对比检测
  return hasPropsChanged(prevProps, nextProps);
}

hasPropsChanged

参数:

  1. prevProps,老节点的props

  2. nextProps,新节点的props

返回值:布尔值,代表props是否改变

作用:

  1. 比较prevProps和nextProps的长度,如果不同则肯定不同返回true

  2. 遍历新的props的key,如果nextProps[key] !== prevProps[key]不相等则返回true

  3. 返回false

function hasPropsChanged(prevProps, nextProps): boolean {
  // 依次对比每一个 props.key

  // 提前对比一下 length ,length 不一致肯定是需要更新的
  const nextKeys = Object.keys(nextProps);
  if (nextKeys.length !== Object.keys(prevProps).length) {
    return true;
  }

  // 只要现在的 prop 和之前的 prop 不一样那么就需要更新
  for (let i = 0; i < nextKeys.length; i++) {
    const key = nextKeys[i];
    if (nextProps[key] !== prevProps[key]) {
      return true;
    }
  }
  return false;
}

apiInject

provide

参数:

  1. key,父组件提供给子组件的key

  2. value,父组件提供给子组件对应key的value

返回值:无

作用:提供变量

  1. 调用getCurrentInstance获取当前的正在执行setup的组件实例currentInstance

  2. 若currentInstance有值,则从组件实例中取出provides和parentProvides父组件的provides

  3. 若parentProvides和provides相等,则把parent.provides作为currentInstance.provides的原型重新赋值(因为provides 初始化的时候是在 createComponent 时处理的,当时是直接把 parent.provides 赋值给组件的 provides 的,详见崔老师源码中的注释)

  4. 把value赋值给provides[key]

export function provide(key, value) {
  const currentInstance = getCurrentInstance();

  if (currentInstance) {
    let { provides } = currentInstance;

    const parentProvides = currentInstance.parent?.provides;

    // 这里要解决一个问题
    // 当父级 key 和 爷爷级别的 key 重复的时候,对于子组件来讲,需要取最近的父级别组件的值
    // 那这里的解决方案就是利用原型链来解决
    // provides 初始化的时候是在 createComponent 时处理的,当时是直接把 parent.provides 赋值给组件的 provides 的
    // 所以,如果说这里发现 provides 和 parentProvides 相等的话,那么就说明是第一次做 provide(对于当前组件来讲)
    // 我们就可以把 parent.provides 作为 currentInstance.provides 的原型重新赋值
    // 至于为什么不在 createComponent 的时候做这个处理,可能的好处是在这里初始化的话,是有个懒执行的效果(优化点,只有需要的时候在初始化)
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides);
    }

    provides[key] = value;
  }
}

inject

参数:

  1. key

  2. defaultValue,默认值(若父组件没有这个值,则使用默认值)

返回值:返回对应父组件提供的key

作用:注入变量

  1. 调用getCurrentInstance获取当前的正在执行setup的组件实例currentInstance

  2. 若currentInstance有值,则从组件实例中取出父组件的provides

  3. 如果父组件提供了对应变量则使用

  4. 如果没有则使用defaultValue

export function inject(key, defaultValue) {
  const currentInstance = getCurrentInstance();

  if (currentInstance) {
    const provides = currentInstance.parent?.provides;

    if (key in provides) {
      return provides[key];
    } else if (defaultValue) {
      if (typeof defaultValue === "function") {
        return defaultValue();
      }
      return defaultValue;
    }
  }
}

 类似资料: