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 属性,如果找不到,则使用默认值。

相关推荐
GIS程序媛—椰子9 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00115 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端18 分钟前
Content Security Policy (CSP)
前端·javascript·面试
木舟100922 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤439132 分钟前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习
半开半落1 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt