阅读 532

从零手写简易Vue3(一)—— crateApp() & mount()

本文使用的vue版本为3.0.2

从源码分析 crateApp()和 mount()做了什么

主入口

// packages/vue/src/index.ts
export * from "@vue/runtime-dom";
复制代码
// packages/runtime-dom/src/index.ts

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args);

  // 省略
}) as CreateAppFunction<Element>;
复制代码
function ensureRenderer() {
  return (
    //首次启动、注册根实例时会创建一个新的renderer
    renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
  );
}
复制代码
// packages/runtime-core/src/renderer.ts

export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options);
}
复制代码

baseCreateRenderer方法非常的长,有 1800+行,进行了大量的函数声明,这里对函数体进行了省略。

函数命名非常语义化,可以清晰得知baseCreateRenderer主要是声明了一些模板编译patch算法相关的函数。

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  const patch = () => {};
  const processText = () => {};
  const processCommentNode = () => {};
  const mountStaticNode = () => {};

  /**
   * Dev / HMR only
   */
  const patchStaticNode = () => {};
  const moveStaticNode = () => {};
  const removeStaticNode = () => {};

  const processElement = () => {};
  const mountElement = () => {};
  const setScopeId = () => {};
  const mountChildren = () => {};
  const patchElement = () => {};
  const patchBlockChildren = () => {};
  const patchProps = () => {};
  const processFragment = () => {};
  const processComponent = () => {};
  const mountComponent = () => {};
  const updateComponent = () => {};
  const setupRenderEffect = () => {};
  const updateComponentPreRender = () => {};
  const patchChildren = () => {};
  const patchUnkeyedChildren = () => {};
  const patchKeyedChildren = () => {};
  const move = () => {};
  const unmount = () => {};
  const remove = () => {};
  const removeFragment = () => {};
  const unmountComponent = () => {};
  const unmountChildren = () => {};
  const getNextHostNode = () => {};
  const render = () => {};
  const internals = {};
  let hydrate = {};
  let hydrateNode = {};

  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate),
  };
}
复制代码

这里createApp方法是调用createAppAPI返回的。

// packages/runtime-core/src/apiCreateApp.ts

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // 接收两个参数,第一个是根实例的配置对象,第二个是props

    if (rootProps != null && !isObject(rootProps)) {
      // 校验props类型
      __DEV__ && warn(`root props passed to app.mount() must be an object.`);
      rootProps = null;
    }

    /**
     * createAppContext()创建App上下文,返回一个具有
     * app、config、mixins、components、directives、provides属性的对象
     */
    const context = createAppContext();

    // set记录已安装的插件
    const installedPlugins = new Set();

    let isMounted = false;

    /**
     * 给context.app初始化一个对象,并赋值给一个新的变量app
     */
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,

      version,

      get config() {},

      set config(v) {},

      use() {},

      mixin() {},

      component() {},

      directive() {},

      mount() {},

      unmount() {},

      provide(key, value) {},
    });

    return app;
  };
}
复制代码

再回到cerateApp()

// packages/runtime-dom/src/index.ts

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args);

  /**
   * 给app.config添加isNativeTag属性,用于开发环境的组件名称校验
   */
  if (__DEV__) {
    injectNativeTagCheck(app);
  }

  /**
   * 保存app.mount方法,将原来的重写
   */
  const { mount } = app;
  app.mount = () => {};

  return app;
}) as CreateAppFunction<Element>;
复制代码

创建 app 时的mount()方法做了什么?

const app = {
  mount(rootContainer: HostElement, isHydrate?: boolean): any {
    // isMounted初始化为false
    if (!isMounted) {
      // 创建VNode(AST,用jsObject表示DOM元素的方法)
      const vnode = createVNode(rootComponent as ConcreteComponent, rootProps);

      // 初始化挂载时将app的上下文环境存储在vnode根节点上
      vnode.appContext = context;

      // 开发环境添加reload方法提升效率
      if (__DEV__) {
        context.reload = () => {
          render(cloneVNode(vnode), rootContainer);
        };
      }

      // 根据不同的运行环境执行不同的渲染VNode流程
      if (isHydrate && hydrate) {
        hydrate(vnode as VNode<Node, Element>, rootContainer as any);
      } else {
        render(vnode, rootContainer);
      }
      isMounted = true;
      app._container = rootContainer;

      // for devtools and telemetry
      (rootContainer as any).__vue_app__ = app;

      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        devtoolsInitApp(app, version);
      }

      // 返回根组件实例的一个代理
      return vnode.component!.proxy;
    } else if (__DEV__) {
      warn(
        `App has already been mounted.\n` +
          `If you want to remount the same app, move your app creation logic ` +
          `into a factory function and create fresh app instances for each ` +
          `mount - e.g. \`const createMyApp = () => createApp(App)\``
      );
    }
  },
};
复制代码

重写了的mount()方法又做了什么?

// 将原来的mount方法保存下来
const { mount } = app;
// web平台重写mount逻辑
app.mount = (containerOrSelector: Element | string): any => {
  const container = normalizeContainer(containerOrSelector);
  if (!container) return;
  const component = app._component;
  // render函数和tempalte选项在渲染时的优先级高于挂载元素的InnerHTML
  if (!isFunction(component) && !component.render && !component.template) {
    component.template = container.innerHTML;
  }
  // clear content before mounting
  container.innerHTML = "";
  const proxy = mount(container);
  container.removeAttribute("v-cloak");
  container.setAttribute("data-v-app", "");

  // 返回实例,调用app.mount()后可以链式调用其他实例方法
  return proxy;
};
复制代码

至此,createApp()方法的逻辑已经梳理完成,此时调用app.mount()方法即可将 app 挂载在 DOM 元素上。

总结一下 createApp()和 mount()做的工作:

  • 创建 Renderer
  • 创建实例
  • 将模板解析成 VNode
  • 根据不同的平台将 VNode 渲染回 dom
  • 将 dom 挂载到元素

实现

假设我们有如下 HTML 结构:

<div id="app">Hello mini-vue</div>
复制代码

createApp()

const createApp = function (...args) {
  const render = createRender();
  const app = {
    version: "0.0.1",
    mount(selector) {
      const container = document.querySelector(selector);
      const vnode = createVNode(container.innerHTML);
      container.innerHTML = "";
      render(vnode, container);
      return this;//此处实际上返回的应是app经过proxy的一个代理
    },
  };
  return app;
};
复制代码

createRender()

function createRender() {
  return function render(vn, dom) {
    if (vn.tag !== "") {
      const tag = document.createElement(vn.tag);
      const text = document.createTextNode(...vn.children);
      tag.appendChild(text);
      dom.appendChild(tag);
    } else {
      const text = document.createTextNode(...vn.children);
      dom.appendChild(text);
    }
  };
}
复制代码

createVNode()

function createVNode(){
    // 维护一个栈,通过进栈出栈匹配嵌套标签
    const stack = [];
    // root节点
    let obj;

    let content = temp;
    while (content.length) {
      if (content.indexOf("<") > 0) {
        // 以文字开头
        const index = content.indexOf("<");
        let text = content.substring(0, index);
        content = content.substring(index);
        if (stack.length) {
          stack[stack.length - 1].children.push(text);
        } else {
          obj.children.push(text);
        }
      } else if (content.indexOf("<") < 0) {
        // 纯文字
        if (stack.length) {
          stack[stack.length - 1].children.push(content);
        } else {
          obj = {
            tag: "",
            children: [],
          };
          obj.children.push(content);
        }
        content = "";
      } else {
        // 以标签开头
        const endIndex = content.indexOf(">");
        let currentTag = content.substring(1, endIndex);
        if (currentTag.indexOf("/") < 0) {
          // 开始标签
          stack.push({
            tag: currentTag,
            children: [],
          });
        } else {
          // 结束标签
          let child = stack[stack.length - 1];

          obj = child;
          stack.pop();
        }
        content = content.substring(endIndex + 1);
      }
    }
    return obj;
}
复制代码

在线demo

如有错误,欢迎指正!

文章分类
前端
文章标签