createApp 流程 (创建渲染器,调用mount)

本文详细解析了Vue.js3.0中createApp的使用方法,包括创建app对象、重写mount方法以及核心渲染流程,展示了如何通过createAppAPI创建和挂载组件,强调了跨平台渲染和组件生命周期管理的关键点。
  • createApp

    // 在 Vue.js 3.0 中,初始化一个应用的方式如下
    import { createApp } from 'vue'
    import App from './app'
    const app = createApp(App)
    app.mount('#app')
    
    createApp 可选两个参数
    在这里插入图片描述

createApp 源码内部总体概览

const createApp = (...args) => {
  // 1.创建 app 对象
  const app = ensureRenderer().createApp(...args);
  const { mount } = app;
  // 2.重写 mount 方法
  app.mount = (containerOrSelector) => {
    // ...
  };
  // 3.返回app实例
  return app;
};

1.创建 app 对象 ,createApp 函数内部流程

// 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法

function ensureRenderer() {
  return renderer || renderer = createRenderer(rendererOptions); // 初始时化走后面,创建渲染器
}

function createRenderer(options) {
  return baseCreateRenderer(options);
}

function baseCreateRenderer(options) {
  	function render(vnode, container) {
    	// 2000行 组件渲染的核心逻辑
	}
  return {
    render,
    createApp: createAppAPI(render), // render就是上两行的这个,此render并不是组件中用到的render。
  };
}

function createAppAPI(render) {
  // createApp createApp 方法接受的两个参数:根组件的对象和 prop
  
  return function createApp(rootComponent, rootProps = null) { // 该createApp 就是暴露给main中调用的方法
    const app = {
      _component: rootComponent,
      _props: rootProps,
      mixin(),
      use(){},
      mount(rootContainer) { // 这个mount在外面被重写了,最终app.mount()调用的是下方重写的mount
        // 创建根组件的 vnode
        const vnode = createVNode(rootComponent, rootProps);
        // 利用渲染器渲染 vnode
        render(vnode, rootContainer);
        app._container = rootContainer;
        return vnode.component.proxy;
      },
    };
    return app;
  };
}
  1. 重写 mount 函数,也可以理解为 对上面的app实例里本来有的mount函数的前置处理
const { mount } = app
// 2. 重写 mount 方法 也是最外面用户调用mount方法的时候,就是调用这个函数
app.mount = (containerOrSelector) => {
    // container是真实的DOM元素 	这一步是对容器标准化
  	 const container = normalizeContainer(containerOrSelector)
	 if (!container) return
		const component = app._component // 组件的options
  		//如果组件对象中没有方法且没有渲染函数且没有模板,那么直接把innerHTML方法作为组件的模板内容
   		if (!isFunction(component) && !component.render && !component.template) {
    		 component.template = container.innerHTML
   		}
  		 // 清空 容器中的内容
  		 container.innerHTML = ''
		 //运行重写后,与平台无关的mount,实现挂载,利用了函数包函数,也就是函数珂理化
   		const proxy = mount(container)
   		if (container instanceof Element) {
    		 // 删除元素上的 v-cloak 指令
     	container.removeAttribute('v-cloak')
     	container.setAttribute('data-v-app', '')
   	}
  	 return proxy
}
  • 这里先进行mount的前置处理(重写)是为了适应平台的一些内容的处理,可以兼容vue2的写法,因为vue不单单是为了web平台服务的,是需要进行跨平台渲染的,因此内部不能够包含任何指定平台的内容,createApp函数内部的app.mount方法是一个标准的可跨平台的组件渲染流程:先创建VNode,再渲染VNode。

  • 首先是通过 normalizeContainer 标准化容器(这里可以传字符串选择器或者 DOM 对象,但如果是字符串选择器,就需要把它转成 DOM 对象,作为最终挂载的容器),然后做一个 if 判断,如果组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容;接着在挂载前清空容器内容,最终再调用 app.mount 的方法走标准的组件渲染流程。
    在这里,重写的逻辑都是和 Web 平台相关的,所以要放在外部实现。此外,这么做的目的是既能让用户在使用 API 时可以更加灵活,也兼容了 Vue.js 2.x 的写法,比如 app.mount 的第一个参数就同时支持选择器字符串和 DOM 对象两种类型。

  1. 返回app实例
return app2
})

上面是 const app = createApp(App) 干的事,总结一下createApp

  1. ensureRenderer() 判断是否为初始化加载,如果是初始化加载就创建渲染器,否则就复用
  2. createRenderer() 没干别的 ,单纯返回baseCreateRenderer()
  3. baseCreateRenderer() 核心渲染逻辑就在这里面,接着返回时,把创建的渲染器给createAppAPI调用createAppAPI(render)
  4. createAppAPI中定义了mixin(),use() 等方法 ,mount方法在后面被重写,返回app实例

从 app.mount(#app)开始,才算真正进入组件渲染流程,那么接下来,我们就重点看一下核心渲染流程做的两件事情:创建 vnode 和渲染 vnode。

mount内部

mount(
     rootContainer: HostElement,
     isHydrate?: boolean,
     isSVG?: boolean
   ): any {
		  // 创建根组件的 vnode
		  const vnode = createVNode(rootComponent, rootProps)
		  
		  // 利用渲染器渲染 vnode
		  render(vnode, rootContainer)
		  
		  app._container = rootContainer
		  return vnode.component.proxy
     }
   },

createVNode 内部

function createVNode(type, props = null, children = null) {
  const vnode = {
    type,
    props,
    ...
};
  // 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
  normalizeChildren(vnode, children);
  return vnode;
}

再看render内部

const render = (vnode, container) => {
  if (vnode == null) {
    // 销毁组件
    if (container._vnode) {
      unmount(container._vnode, null, null, true);
    }
  } else {
  
    // 创建或者更新组件
    patch(container._vnode || null, vnode, container);
  }
};

这个渲染函数 render 的实现很简单,如果它的第一个参数 vnode 为空,则执行销毁组件的逻辑,否则执行创建或者更新组件的逻辑。

着看一下渲染 vnode的 patch 函数

patch内部

const patch = ( n1,n2,container,....) => {
  // 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
  if (n1 && !isSameVNodeType(n1, n2)) {
    anchor = getNextHostNode(n1);
    unmount(n1, parentComponent, parentSuspense, true);
    n1 = null;
  }
      if (n2.shapeFlag & 1 /* ELEMENT */) {
      
        // 渲染普通 DOM 元素
        processElement(
          n1,
          n2,
          container,
        );
      } else if (shapeFlag & 6 /* COMPONENT */) {
        // 渲染组件
        processComponent(
          n1,
          n2,
          container,
        );
      } 
  }
};

1.渲染组件processComponent 函数的实现:

const processComponent = (
  n1,
  n2,
  container,
) => {
  if (n1 == null) {
    // 挂载组件
    mountComponent(
      n2,
      container,
    );
  } else {
    // 更新组件
    updateComponent(n1, n2, parentComponent, optimized);
  }
};

该函数的逻辑很简单,如果 n1 为 null,则执行挂载组件的逻辑,否则执行更新组件的逻辑。

看看其中mountComponent实现

const mountComponent = (
  initialVNode,
  container,
) => {
  // 创建组件实例
  const instance = (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense
  ));
  // 设置组件实例
  setupComponent(instance);
  // 设置并运行带副作用的渲染函数
  setupRenderEffect( );
};

setupRenderEffect带副作用的渲染函数内部

const setupRenderEffect = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 创建响应式的副作用渲染函数
  instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
      // 渲染组件生成子树 vnode
      const subTree = (instance.subTree = renderComponentRoot(instance));
      // 把子树 vnode 挂载到 container 中
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
      // 保留渲染生成的子树根 DOM 节点
      initialVNode.el = subTree.el;
      instance.isMounted = true;
    } else {
      // 更新组件
    }
  }, prodEffectOptions);
};

这里当组件的数据发生变化时,effect 函数包裹的内部渲染函数 componentEffect 会重新执行一遍,从而达到重新渲染组件的目的。

setupRenderEffect 主要做两件事情渲染组件生成 subTree、把 subTree 挂载到 container 中。

知识点:

  • 节点渲染生成的 vnode ,对应的就是 Hello 组件的 initialVNode eg : p标签
  • 组件内部整个 DOM 节点对应的 vnode 就是执行 renderComponentRoot 渲染生成对应的 subTree,我们可以把它称作“子树 vnode”

每个组件都会有对应的 render 函数, template也会编译成 render 函数,而 renderComponentRoot 函数就是去执行 render 函数创建整个组件树内部的 vnode,把这个 vnode 再经过内部一层标准化,就得到了该函数的返回结果:子树 vnode

渲染生成子树 vnode 后,接下来就是把 vnode 挂载到 container 中了。

2.渲染普通 DOM元素processElement 函数

const processElement = (
  n1,
  n2,
  container,
) => {
  if (n1 == null) {
    //挂载元素节点
    mountElement();
  } else {
    //更新元素节点
    patchElement();
  }
};

该函数的逻辑很简单,如果 n1 为 null,走挂载元素节点的逻辑,否则走更新元素节点逻辑。

mountElement()内部实现

const mountElement = (
  vnode,
  container,
) => {
  // 创建 DOM 元素节点
  let el = vnode.el = hostCreateElement();
  
  if (props) {
    // 处理 props,比如 class、style、event 等属性
  }
  if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
    // 处理子节点是数组的情况
    mountChildren(
    // 循环递归 patch 挂载 child
		patch(
		      null,
		      child,
    	);
);
  }
  // 把创建的 DOM 元素节点挂载到 container 上
  hostInsert(el, container, anchor);
};

hostCreateElement() 它调用了底层的 DOM API document.createElement 创建元素,所以本质上 Vue.js 强调不去操作 DOM ,只是希望用户不直接碰触 DOM,vue来帮你操作 DOM。

最后app.mount()流程结束 ,总结一下mount()

  1. createVNode(rootComponent, rootProps) 拿到元素和属性,创建节点
  2. render(vnode, container) 拿到上面创建好的节点,render渲染器开始渲染
  3. patch() 创建或者更新组件,内部根据元素的类型不同来调用不同的方法渲染
    • processComponent () 渲染组件
      • mountComponent () 创建组件实例、设置组件实例、设置并运行带副作用的渲染函数setupRenderEffect
      • setupRenderEffect() 当组件的数据发生变化时,渲染组件生成 subTree(子树 vnode)、把 subTree 挂载到 container 中。
    • processElement () 渲染html元素
      • mountElement () 调用hostCreateElement() 创建节点, 处理 props、处理 children、挂载 DOM 元素到 container 上。
      • hostCreateElement() 调用了底层的 DOM API document.createElement 创建元素
  4. patch 构造完成DOM 树,完成组件的渲染。
  5. 最后通过 hostInsert ()方法把构造 DOM树挂载到 container 上
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值