Vue3 源码解读之其他实现原理

1. 异步组件实现原理

defineAsyncComponent函数是一个高阶组件 ,他的返回值是一个包装组件。此包装组件会根据状态来决定渲染的内容,加载成功后渲染组件 ,在未渲染成功时渲染一个占位符节点

1.1 基本用法

使用:

js 复制代码
let asyncComponent = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        render: () => h("div", "hi james"),
      });
    }, 1000);
  });
});

实现:

js 复制代码
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<
  T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
  if (isFunction(source)) {
    source = { loader: source }
  }

  const {
    loader,
    loadingComponent,
    errorComponent,
    delay = 200,
    timeout, // undefined = never times out
    suspensible = true,
    onError: userOnError
  } = source

  const load = (): Promise<ConcreteComponent> => {
    let thisRequest: Promise<ConcreteComponent>;
    // ... 省略部分代码,下面再说明
  }
  return defineComponent({
    name: 'AsyncComponentWrapper',
    // ... 省略部分代码
    setup() {
      const loaded = ref(false);
      // 执行这个Promise,等待promise的执行结果,等待之前渲染片段,等待成功后,将成功态的结果赋给要渲染的组件
      load()
        .then(() => {
          loaded.value = true;
          if (instance.parent && isKeepAlive(instance.parent.vnode)) {
            // 判断当前组件的父组件是否为一个 keep-alive 组件,并且该 keep-alive 组件的 vnode 属性存在。如果是的话,我们会强制更新父组件,以便让 loaded 组件的名称也被考虑在内。
            queueJob(instance.parent.update);
          }
        })
        .catch((err) => {
          // 处理异步加载组件时可能出现的错误。如果出现错误,我们会调用 onError() 方法来进行错误处理,并将 error.value 属性设置为错误对象
          onError(err);
          error.value = err;
        });
      return () => {
        if (loaded.value && resolvedComp) {
          return createInnerComp(resolvedComp, instance);
        } else if (error.value && errorComponent) {
          return createVNode(errorComponent, {
            error: error.value,
          });
        } else if (loadingComponent && !delayed.value) {
          return createVNode(loadingComponent);
        }
      };
    }
  })
}

1.2 异步组件超时处理

js 复制代码
let asyncComponent = defineAsyncComponent({
  loader: () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({
          render() {
            return h("div", "hi james");
          },
        });
      }, 1000);
    });
  },
  timeout: 2000, // 异步组件超时时间
  errorComponent: {
    render: () => h("Text", "超时错误"),
  },
});
js 复制代码
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<
  T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
  if (isFunction(source)) {
    source = { loader: source }
  }
  return defineComponent({
    setup() {
      const loaded = ref(false)
      const error = ref(); // 是否超时
      if (timeout != null) {
        // 根据超时时间,开个定时器,时间一到就标记已超时
        setTimeout(() => {
          if (!loaded.value && !error.value) {
            const err = new Error(
              `Async component timed out after ${timeout}ms.`
            )
            onError(err)
            error.value = err // 显示错误组件
          }
        }, timeout)
      }
      return () => {
        if (loaded.value && resolvedComp) {
          return createInnerComp(resolvedComp, instance)
        } else if (error.value && errorComponent) {
          return createVNode(errorComponent, {
            error: error.value
          })
        } else if (loadingComponent && !delayed.value) {
          return createVNode(loadingComponent)
        }
      }
    },
  });
}

组件卸载的时候需要稍作处理

js 复制代码
const unmount = (vnode) => {
  const { shapeFlag } = vnode;
  if (vnode.type === Fragment) {
    return unmountChildren(vnode.children);
  } else if (shapeFlag & ShapeFlags.COMPONENT) {
    // 如果是组件的话就移除
    return unmount(vnode.component.subTree); // 移除组件
  }
  hostRemove(vnode.el);
};

1.3 异步组件 loading 处理

js 复制代码
let asyncComponent = defineAsyncComponent({
  loader: () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({
          render() {
            return h("div", "hi james");
          },
        });
      }, 3000);
    });
  },
  timeout: 2000,
  errorComponent: {
    render: () => h("Text", "超时才显示错误"),
  },
  delay: 1000,
  loadingComponent: {
    render: () => h("h2", "loading...."),
  },
});
js 复制代码
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<
  T extends Component = { new(): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
  if (isFunction(source)) {
    source = { loader: source }
  }

  const {
    loader,
    loadingComponent,
    errorComponent,
    delay = 200,
    timeout, // undefined = never times out
    suspensible = true,
    onError: userOnError
  } = source
  const loaded = ref(false)
  const error = ref()
  const delayed = ref(!!delay)

  // 延时显示loading
  if (delay) {
    setTimeout(() => {
      delayed.value = false
    }, delay)
  }

  return () => {
    if (loaded.value && resolvedComp) {
        // 组件加载完毕显示
      return createInnerComp(resolvedComp, instance)
    } else if (error.value && errorComponent) {
        // 显示错误组件
      return createVNode(errorComponent, {
        error: error.value
      })
    } else if (loadingComponent && !delayed.value) {
        // 显示loading组件
      return createVNode(loadingComponent)
    }
  }
}

1.4 异步组件加载重试处理

js 复制代码
let asyncComponent = defineAsyncComponent({
  loader: () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject({
          render() {
            return h("div", "hi james");
          },
        });
      }, 3000);
    });
  },
  timeout: 2000,
  errorComponent: {
    render: () => h("Text", "超时错误"),
  },
  delay: 1000,
  loadingComponent: {
    render: () => h("h2", "loading...."),
  },
  onError(retry) {
    console.log("错了");
    retry();
  },
});
js 复制代码
/*! #__NO_SIDE_EFFECTS__ */
export function defineAsyncComponent<
  T extends Component = { new(): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
  if (isFunction(source)) {
    source = { loader: source }
  }

  const {
    loader,
    loadingComponent,
    errorComponent,
    delay = 200,
    timeout, // undefined = never times out
    suspensible = true,
    onError: userOnError
  } = source

  let pendingRequest: Promise<ConcreteComponent> | null = null
  let resolvedComp: ConcreteComponent | undefined

  let retries = 0
  const retry = () => {
    retries++
    pendingRequest = null
    return load()
  }

  const load = (): Promise<ConcreteComponent> => {
    let thisRequest: Promise<ConcreteComponent>
    return (
      pendingRequest ||
      (thisRequest = pendingRequest =
        loader()
          .catch(err => {
            err = err instanceof Error ? err : new Error(String(err))
            if (userOnError) {
              return new Promise((resolve, reject) => {
                // 重新加载包装为成功状态的promise作为函数继续执行
                const userRetry = () => resolve(retry())
                const userFail = () => reject(err)
                userOnError(err, userRetry, userFail, retries + 1)
              })
            } else {
              throw err
            }
          })
          .then((comp: any) => {
            // ... 省略部分代码
          }))
    )
  }
}

2. 函数式组件实现原理

函数式组件(functional-components)本质就是一个函数函数的返回值就是虚拟 DOM 。 在 Vue 3 中,所有的函数式组件都是用普通函数创建 的。换句话说,不需要定义 { functional: true } 组件选项。

js 复制代码
export const createVNode = (type, props, children = null) => {
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : isObject(type)
    ? ShapeFlags.STATEFUL_COMPONENT // 带状态的对象形式组件(业务组件用的多)
    : isFunction(type)
    ? ShapeFlags.FUNCTIONAL_COMPONENT // 纯函数组件
    : 0;
  // 创建虚拟节点是
};
js 复制代码
function initProps(instance, propsOptions, propsData) {
  // ... 属性初始化的时候如果是函数式组件,则 attrs 就是函数式组件的props
  if (instance.vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) {
    instance.props = attrs;
  }
}

产生subTree时, 要根据类型做不同的处理

js 复制代码
export function renderComponentRoot(instance) {
  let { render, proxy, vnode, props } = instance;
  if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    return render.call(proxy, proxy);
  } else {
    return vnode.type(props); // 函数式组件直接调用即可
  }
}
const subTree = renderComponentRoot(instance);

3. Provide/Inject 原理

在 vue3 中可以使用 provideinject 来替代 vue2 的全局属性挂载 app.config.globalProperties

Tips: ⚠️ 依赖注入时,注入的数据来源不明确(需要指定唯一的 key 值)。不适合在业务代码中使用,可能会带来重构复杂的问题。(一般用于编写插件使用)

3.1 Vue3 中依赖注入原理

在创建实例时会采用父组件的provides属性

js 复制代码
const instance: ComponentInternalInstance = {
  uid: uid++,
  provides: parent ? parent.provides : Object.create(appContext.provides),
};

3.2 Provide 源码分析

provides属性会向上查找父组件provides属性

Provide 源码

js 复制代码
export function provide<T, K = InjectionKey<T> | string | number>(
  key: K,
  value: K extends InjectionKey<infer V> ? V : T
) {
  if (!currentInstance) {
    if (__DEV__) {
      // Provide 只能在 setup 方法里面使用
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides // 默认的情况下,当前实例会继承父类的provides对象
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) { // 如果组件提供自己的provide的时候,就会用父组件的provides作为原型链创建新的对象
      // 然后在 inject 中,我们可以从实例中查找注入父函数,让原型链做这些工作。
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    // 在TS中不允许符号来作为索引类型
    provides[key as string] = value
  }
}

3.3 Inject 源码分析

会查找provides中是否包含需要注入的属性

Inject 源码

如何获取 provide 的值呢?

重点:如果当前实例是根节点,那就会回退到当前实例的上下文中拿;否则就是从父级实例的 provides 中拿

js 复制代码
export function inject(
  key: InjectionKey<any> | string, // 需要注入的属性
  defaultValue?: unknown, // 提供属性的默认值
  treatDefaultAsFactory = false // 属性默认值是否为工厂函数形式
) {
  // 这里回退到currentRenderingInstance,是为了便于 functional components 来调用
  const instance = currentInstance || currentRenderingInstance

  // 支持从 app-level 的 provides 中查找,使用 app.runWithContext() 方法
  if (instance || currentApp) {
    // 也支持 app.use 插件
    // 重点:获取provides值:如果当前实例是根节点,那就会回退到当前实例的上下文中拿;否则就是从父级实例的provides中拿
    const provides = instance
      ? instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides
      : currentApp!._context.provides

    if (provides && (key as string | symbol) in provides) {
      // 在TS中不允许符号来作为索引类型
      return provides[key as string] // 则将属性返回
    } else if (arguments.length > 1) { // 注入式可以采用默认参数注入
      // provides中找不到注入的属性,就读取默认值(可以支持传入一个函数)
      return treatDefaultAsFactory && isFunction(defaultValue)
        ? defaultValue.call(instance && instance.proxy)
        : defaultValue
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
    }
  } else if (__DEV__) {
    // inject 只能在 setup 方法里面或者函数组件中使用
    warn(`inject() can only be used inside setup() or functional components.`)
  }
}

总结

本文主要介绍了 Vue3 中的异步组件、函数式组件和依赖注入的实现原理。

1、异步组件实现原理:Vue3 使用 defineAsyncComponent 函数定义异步组件,该函数返回一个包装组件,根据状态决定渲染内容。异步组件支持超时处理、加载状态处理、重试处理等功能。

2、函数式组件实现原理:Vue3 中的函数式组件是一个返回虚拟 DOM 的普通函数,不需要定义 { functional: true } 组件选项。在渲染组件时,根据 vnode.shapeFlag 的类型进行处理。

3、Provide/Inject 原理:Vue3 使用 provideinject 方法实现依赖注入。在创建实例时,会采用父组件的 provides 属性。provide 方法用于在当前实例中提供一个值,而 inject 方法用于在当前实例中注入一个值。注入时会向上查找父组件的 provides 属性,如果找不到,则使用默认值。

相关推荐
不爱吃糖的程序媛1 小时前
浅谈前端架构设计与工程化
前端·前端架构设计
BillKu3 小时前
Vue3 Element Plus 对话框加载实现
javascript·vue.js·elementui
郝YH是人间理想3 小时前
系统架构设计师案例分析题——web篇
前端·软件工程
Evaporator Core3 小时前
深入探索:Core Web Vitals 进阶优化与新兴指标
前端·windows
初遇你时动了情4 小时前
html js 原生实现web组件、web公共组件、template模版插槽
前端·javascript·html
QQ2740287564 小时前
Soundness Gitpod 部署教程
linux·运维·服务器·前端·chrome·web3
前端小崔4 小时前
从零开始学习three.js(18):一文详解three.js中的着色器Shader
前端·javascript·学习·3d·webgl·数据可视化·着色器
哎呦你好4 小时前
HTML 表格与div深度解析区别及常见误区
前端·html
运维@小兵4 小时前
vue配置子路由,实现点击左侧菜单,内容区域显示不同的内容
前端·javascript·vue.js
koiy.cc5 小时前
记录:echarts实现tooltip的某个数据常显和恢复
前端·echarts