Vue3.0源码逐行解析(二) 组件的本质

686 阅读5分钟

在上篇文章已经介绍了 Vue2.x 与 Vue3 的从创建应用实例到挂载的过程,本章节会介绍组件渲染流程。

组件的本质

当我们使用 Vue 或 React 时,往往会将页面拆分为各种组件,通过拼装组件来形成页面和应用,就像搭积木一样。那大家是否思考过:"组件的产出是什么?"

拿 Vue 来说 , 一个组件最核心的东西是 render 函数,剩余的其他内容,如 data compouted、props 等都是为 render 函数提供数据来源服务的。render 函数产出了 Virtual DOM借助 snabbdom 的 API 我们可以很容易地用代码描述这个过程:

import { h } from 'snabbdom'

// h 函数用来创建 VNode,组件的产出是 VNode
const MyComponent = props => {
  return h('h1', props.title)
}

Virtual DOM 要渲染成真实的 DOM trre 我们通常把这个过程叫做 patch,同样可以借助 snabbdom 的 API 复刻下这个过程:

import { h, init } from 'snabbdom'
// init 方法用来创建 patch 函数
const patch = init([])

const MyComponent = props => {
  return h('h1', props.title)
}

// 组件的产出是 VNode
const prevVnode = MyComponent({ title: 'prev' })
// 将 VNode 渲染成真实 DOM
patch(document.getElementById('app'), prevVnode)

当数据变更时,组件会产出新的VNode,我们只需再次调用patch函数即可:

// 数据变更,产出新的 VNode
const nextVnode = MyComponent({ title: 'next' })
// 通过对比新旧 VNode,高效地渲染真实 DOM
patch(prevVnode, nextVnode)

以上就是我们要达成的共识:组件的产出就是 Virtual DOM。为何组件要产出 Virtual DOM 呢?其原因是 Virtual DOM 带来了 分层设计,它对渲染过程的抽象,使得框架可以渲染到 web(浏览器) 以外的平台(SSR Weex 小程序)。

组件的 VNode 如何表示

Vue 通过建立一个虚拟 DOM来追踪自己要如何改变真实 DOM。

return createElement('h1', this.blogTitle)

createElement到底会返回什么呢?其实不是一个实际的DOM 元素。它更准确的名字可能是createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为"VNode"。"虚拟 DOM"是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

TIP 虚拟DOM:渲染函数 & JSX — Vue.js

创建VNode

通过官方解释可以知道 vnode 本质上是用来描述 DOM 的 JavaScript 对象,它在 Vue.js 中可以描述不同类型的节点,比如普通元素节点、组件节点等。

什么是普通元素节点呢?举个例子,在 HTML 中我们使用 标签来写一个按钮:

<button class="fist">click counter</button>

我们可以用 vnode 这样表示

标签:

const vnode = {
  type: 'button',
  props: { 
    'class': 'fist',
  },
  children: ['click counter']
}

其中 type 属性表示 DOM 的标签类型,props 属性表示 DOM 的一些附加信息,比如 style 、class 等,children 属性表示 DOM 的子节点,如果元素只有一个子节点且是文本的情况,可以用字符串表示 。

什么是组件节点呢?其实, vnode 除了可以像上面那样用于描述一个真实的 DOM,也可以用来描述组件。

<blog-post title='Why Vue is so fun'></blog-post>

我们可以用 vnode 这样表示 组件标签:

const blogPost= {
  // 在这里定义组件对象
}
const vnode = {
  type: blogPost,
  props: { 
    msg: 'Why Vue is so fun'
  }
}

组件 vnode 其实是对抽象事物的描述,这是因为我们并不会在页面上真正渲染一个 标签,而是渲染组件内部定义的 HTML 标签。 除此之外 vnode 类型,还有纯文本 vnode、注释 vnode 等等,在编译的过程会把元素的文本、注释作为子节点转化成vnode对象统一管理。

在Vue3 针对 vnode 的 type,做了更详尽的分类,包括 Suspense、Teleport 等,且把 vnode 的类型信息做了编码,以便在后面的 patch 阶段,可以根据不同的类型执行相应的处理逻辑:

const shapeFlag = isString(type)
  ? 1 /* ELEMENT */
  : isSuspense(type)
    ? 128 /* SUSPENSE */
    : isTeleport(type)
      ? 64 /* TELEPORT */
      : isObject(type)
        ? 4 /* STATEFUL_COMPONENT */
        : isFunction(type)
          ? 2 /* FUNCTIONAL_COMPONENT */
          : 0

我们已经了解了 VNode ,那么 Vue 内部是如何创建这些 vnode 的呢?回顾 app.mount 函数的实现,内部是通过 createVNode 函数创建了根组件的 vnode :

function createApp(rootComponent, rootProps = null) {
	const app = {
		_component: rootComponent,
		_props: rootProps,
		mount(rootContainer) {
			// 创建根组件的 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, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
      if (!type || type === NULL_DYNAMIC_COMPONENT) {
          if ( !type) {
              warn(`Invalid vnode type when creating vnode: ${type}.`);
          }
          type = Comment;
      }
      if (isVNode(type)) {
          // createVNode receiving an existing vnode. This happens in cases like
          // <component :is="vnode"/>
          // #2078 make sure to merge refs during the clone instead of overwriting it
          const cloned = cloneVNode(type, props, true /* mergeRef: true */);
          if (children) {
              normalizeChildren(cloned, children);
          }
          return cloned;
      }
      // class component normalization.
      if (isClassComponent(type)) {
          type = type.__vccOpts;
      }
      // class & style normalization.
      if (props) {
          // for reactive or proxy objects, we need to clone it to enable mutation.
          if (isProxy(props) || InternalObjectKey in props) {
              props = extend({}, props);
          }
          let { class: klass, style } = props;
          if (klass && !isString(klass)) {
              props.class = normalizeClass(klass);
          }
          if (isObject(style)) {
              // reactive state objects need to be cloned since they are likely to be
              // mutated
              if (isProxy(style) && !isArray(style)) {
                  style = extend({}, style);
              }
              props.style = normalizeStyle(style);
          }
      }
      // encode the vnode type information into a bitmap
      const shapeFlag = isString(type)
          ? 1 /* ELEMENT */
          :  isSuspense(type)
              ? 128 /* SUSPENSE */
              : isTeleport(type)
                  ? 64 /* TELEPORT */
                  : isObject(type)
                      ? 4 /* STATEFUL_COMPONENT */
                      : isFunction(type)
                          ? 2 /* FUNCTIONAL_COMPONENT */
                          : 0;
      if ( shapeFlag & 4 /* STATEFUL_COMPONENT */ && isProxy(type)) {
          type = toRaw(type);
          warn(`Vue received a Component which was made a reactive object. This can ` +
              `lead to unnecessary performance overhead, and should be avoided by ` +
              `marking the component with \`markRaw\` or using \`shallowRef\` ` +
              `instead of \`ref\`.`, `\nComponent that was made reactive: `, type);
      }
      const vnode = {
          __v_isVNode: true,
          ["__v_skip" /* SKIP */]: true,
          type,
          props,
          key: props && normalizeKey(props),
          ref: props && normalizeRef(props),
          scopeId: currentScopeId,
          children: null,
          component: null,
          suspense: null,
          ssContent: null,
          ssFallback: null,
          dirs: null,
          transition: null,
          el: null,
          anchor: null,
          target: null,
          targetAnchor: null,
          staticCount: 0,
          shapeFlag,
          patchFlag,
          dynamicProps,
          dynamicChildren: null,
          appContext: null
      };
      // validate key
      if ( vnode.key !== vnode.key) {
          warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type);
      }
      normalizeChildren(vnode, children);
      // normalize suspense children
      if ( shapeFlag & 128 /* SUSPENSE */) {
          const { content, fallback } = normalizeSuspenseChildren(vnode);
          vnode.ssContent = content;
          vnode.ssFallback = fallback;
      }
      if (shouldTrack$1 > 0 &&
          // avoid a block node from tracking itself
          !isBlockNode &&
          // has current parent block
          currentBlock &&
          // presence of a patch flag indicates this node needs patching on updates.
          // component nodes also should always be patched, because even if the
          // component doesn't need to update, it needs to persist the instance on to
          // the next vnode so that it can be properly unmounted later.
          (patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
          // the EVENTS flag is only for hydration and if it is the only flag, the
          // vnode should not be considered dynamic due to handler caching.
          patchFlag !== 32 /* HYDRATE_EVENTS */) {
          currentBlock.push(vnode);
      }
      return vnode;
  }

先从参数下手调用 createVNode(rootComponent, rootProps) 传递 rootComponent 这是什么?

举个例子假设我们通过一个最简单的例子去创建Vue的应用实例:

<!DOCTYPE html>
<html>
   <head>
	<meta charset="utf-8">
	<title></title>
   </head>
      <body>
	<div id="app">{{ message }}</div>
	<script src="Vue-next.js"></script>
	<script>
	 const conter = {
	  data() {
		return {
		    message: "hello Vue3.0"
		}
	      }
	   }
	  Vue.createApp(conter).mount('#app')
	</script>
      </body>
</html>

rootComponent 接收的就是传递进来的 conter 对象,同时在重写 mount 方法时还扩展了另外一个属性 template 。

const { mount } = app;
      app.mount = (containerOrSelector) => {
          const container = normalizeContainer(containerOrSelector);
          if (!container)
              return;
          const component = app._component;
          if (!isFunction(component) && !component.render && !component.template) {
              component.template = container.innerHTML;
          }
          // clear content before mounting
           ...
          return proxy;
 };

以目前我们所掌握的信息 rootComponent 如下:

{template: "{{ message }}", data: function(){}}

props、 children、patchFlag 、dynamicProps 、isBlockNode 都是默认值。

进入到代码部分:

const NULL_DYNAMIC_COMPONENT = Symbol();
const Comment = Symbol( 'Comment' );

if (!type || type === NULL_DYNAMIC_COMPONENT) {
	if (!type) {
		warn(`Invalid vnode type when creating vnode: ${type}.`);
	}
	type = Comment;
}

如果 type 为undefined 或者 false 或者 type 数据类型为 Symbol 则报错提示用户"Invalid vnode type when creating vnode" (创建vnode时无效的vnode类型)。

接下来检测 type 是不是 VNode 对象是就克隆:

if (isVNode(type)) {
	// createVNode receiving an existing vnode. This happens in cases like
	// <component :is="vnode"/>
	// #2078 make sure to merge refs during the clone instead of overwriting it
	const cloned = cloneVNode(type, props, true /* mergeRef: true */ );
	if (children) {
		normalizeChildren(cloned, children);
	}
	return cloned;
}

所有 VNode 对象都有一个 __v_isVNode 属性,isVNode 方法也是根据这个属性来判断是否为 VNode 对象。

function isVNode(value) {
	return value ? value.__v_isVNode === true : false;
}

如果 type 是函数则标准版类组件。

if (isClassComponent(type)) {
	type = type.__vccOpts;
}

接下来处理 props 相关逻辑,标准化 class 和 style。

if (props) {
	// for reactive or proxy objects, we need to clone it to enable mutation.
	if (isProxy(props) || InternalObjectKey in props) {
		props = extend({}, props);
	}
	let { class: klass, style} = props;
	
	if (klass && !isString(klass)) {
		props.class = normalizeClass(klass);
	}
	if (isObject(style)) {
		// reactive state objects need to be cloned since they are likely to be
		// mutated
		if (isProxy(style) && !isArray(style)) {
			style = extend({}, style);
		}
		props.style = normalizeStyle(style);
	}
}

接下来用学术上说叫bitmap,所谓的Bit-map就是用一个bit位来标记某个元素对应的Value,这里是用来标记vnode类型信息。

const shapeFlag = isString(type) ?
	1 /* ELEMENT */ :
	isSuspense(type) ?
	128 /* SUSPENSE */ :
	isTeleport(type) ?
	64 /* TELEPORT */ :
	isObject(type) ?
	4 /* STATEFUL_COMPONENT */ :
	isFunction(type) ?
	2 /* FUNCTIONAL_COMPONENT */ :
	0;

要理解这个 if 语句先要理解 & 按位与AND 。按位与操作符由一个和号字符(&)表示,它有两个操作符数。从本质上来讲,按位与操作就是将两个数值的每一位对齐,对相同位置上的两个数执行AND操作。

按位与AND操作规则:只有两个数值的对应位都是1时才返回1,任何一位是0,结果都是0。

if (shapeFlag & 4 /* STATEFUL_COMPONENT */ && isProxy(type)) {
	type = toRaw(type);
	warn(`Vue received a Component which was made a reactive object. This can ` +
		`lead to unnecessary performance overhead, and should be avoided by ` +
		`marking the component with \`markRaw\` or using \`shallowRef\` ` +
		`instead of \`ref\`.`, `\nComponent that was made reactive: `, type);
}

如上shapeFlag 有可能 shapeFlag === 1 、shapeFlag === 128 、shapeFlag === 64 、shapeFlag === 4、shapeFlag ===2 、 shapeFlag === 0。

只有 shapeFlag === 4 , shapeFlag & 4 表达式才成立。 接下来就去检测 type 是不是一个被响应式系统侦测的对象, 如果是则输出错误信息。

接下来你讲看到 VNode 真正的形态,归根结底就是一个 JavaScript 对象,它通过一些属性直观地描述清楚当前节点的信息。

const vnode = {
	__v_isVNode: true,
	["__v_skip" /* SKIP */ ]: true,
	type,
	props,
	key: props && normalizeKey(props),
	ref: props && normalizeRef(props),
	scopeId: currentScopeId,
	children: null,
	component: null,
	suspense: null,
	ssContent: null,
	ssFallback: null,
	dirs: null,
	transition: null,
	el: null,
	anchor: null,
	target: null,
	targetAnchor: null,
	staticCount: 0,
	shapeFlag,
	patchFlag,
	dynamicProps,
	dynamicChildren: null,
	appContext: null
};

通过上述分析其实可以看到 createVNode 做的事情很简单,就是:对 props 做标准化处理、对 vnode 的类型信息Bit-map、创建 vnode 对象,标准化子节点 children 。

现在拥有了这个 vnode 对象,接下来就看下如何渲染。

源码:

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

接下来就是去解析 render 函数 这里有个 rootContainer 参数,它是根组件要挂载到的真实DOM节点,在重写 mount 方法中定义。

const createApp = ((...args) => {
	const app = ensureRenderer().createApp(...args); 
	const { mount } = app;
	app.mount = (containerOrSelector) => {
		const container = normalizeContainer(containerOrSelector);
		if (!container)
			return;
		const component = app._component;
		if (!isFunction(component) && !component.render && !component.template) {
			component.template = container.innerHTML;
		}
		// clear content before mounting
		container.innerHTML = '';
                const proxy = mount(container);
                //...
		return proxy;
	};
	return app;
});

你在创建 Vue3 应用实例:

Vue.createApp(/* options */)
Vue.createApp(/* options */).mount('#app')

调用 mount 方法中传入的 "#app" 会作为实参给到 containerOrSelector 。normalizeContainer 的作用就是去找到 id 为 "#app" 的 DOM 元素传递给缓存的 mount 方法。

normalizeContainer

function normalizeContainer(container) {
	if (isString(container)) {
		const res = document.querySelector(container);
		if (!res) {
			warn(`Failed to mount app: mount target selector returned null.`);
		}
		return res;
	}
	return container;
}

搞清楚了 render(vnode, rootContainer) 函数调用时的两个参数,我们去看下 render 函数的内部代码吧!

const render = (vnode, container) => {
	if (vnode == null) {
		if (container._vnode) {
			unmount(container._vnode, null, null, true);
		}
	} else {
		patch(container._vnode || null, vnode, container);
	}
	flushPostFlushCbs();
	container._vnode = vnode;
};

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

接下来我们接着看一下上面渲染 vnode 的代码中涉及的 patch 函数的实现:

const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized =
	false) => {
	// patching & not same type, unmount old tree
	if (n1 && !isSameVNodeType(n1, n2)) {
		anchor = getNextHostNode(n1);
		unmount(n1, parentComponent, parentSuspense, true);
		n1 = null;
	}
	if (n2.patchFlag === -2 /* BAIL */ ) {
		optimized = false;
		n2.dynamicChildren = null;
	}
	const { type, ref, shapeFlag } = n2;
	switch (type) {
		case Text:
			processText(n1, n2, container, anchor);
			break;
		case Comment:
			processCommentNode(n1, n2, container, anchor);
			break;
		case Static:
			if (n1 == null) {
				mountStaticNode(n2, container, anchor, isSVG);
			} else {
				patchStaticNode(n1, n2, container, isSVG);
			}
			break;
		case Fragment:
			processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
			break;
		default:
			if (shapeFlag & 1 /* ELEMENT */ ) {
				processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
			} else if (shapeFlag & 6 /* COMPONENT */ ) {
				processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
			} else if (shapeFlag & 64 /* TELEPORT */ ) {
				type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals);
			} else if (shapeFlag & 128 /* SUSPENSE */ ) {
				type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals);
			} else {
				warn('Invalid VNode type:', type, `(${typeof type})`);
			}
	}
	// set ref
	if (ref != null && parentComponent) {
		setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2);
	}
};

patch 函数也是定义在 baseCreateRenderer 函数中,这个函数的作用跟Vue2 patch 差不多。 分为两个部分 "initial render" 初始渲染, "updates" 数据更新时处理(包含diff),我们目前只分析创建过程,更新过程在后面的章节分析。

patch 函数接受多个参数,目前我们只需要关注 n1、n2、container。

  • n1 表示旧的 VNode,当 n1 为 null 的时候,表示是一次挂载的过程;
  • n2 表示新的 VNode 节点,后续会根据这个 VNode 类型执行更新操作;
  • container 表示 DOM 容器,也就是 VNode 渲染生成 DOM 后,会挂载到 container 中。