Vue的视图渲染逻辑
在 Vue 的渲染逻辑中,主流程分为以下几个步骤:
- 初始化 Mixin
- 响应式代理
- 将 template 模板转换为 render 函数
- 虚拟 DOM 渲染
- 真实 DOM 挂载
- Watcher 和 Dep 的监听
- Diff 算法
- 真实 DOM 的挂载
# 初始化 mixin
Vue 通过 Mixin 的方式来扩展其类的功能。这样的设计使得 Vue 更好地进行模块化管理,而不需要将所有功能都写在同一个文件中。初始化 Mixin 包括以下主要模块:
- init: 用于 Vue 类的初始化以及其模块的初始化。
- state: 处理属性和响应式等。
- render: 处理虚拟 DOM 渲染。
- lifecycle: 处理生命周期以及真实 DOM 挂载,父子节点的关系。
- event: 处理 Vue 的事件系统。
- inject, provide: 处理 Vue 对 Provide 和 Inject API。
这样的模块划分使得 Vue 的功能更好地组织和扩展。
根据重要性,挑选一些模块从源码层面看看 Vue 是如何实现,如何精妙设计此框架。
# 响应式代理
在处理 state
模块中,Vue 对 data
、 watch
、 computed
等属性进行响应式处理,通过 Watcher
和 Dep
对属性进行监听。Vue3 的响应式代理是通过 Proxy
实现的, Vue2 则是通过 Object.defineProperty
。在代理中,当属性被访问或修改时,会触发相应的 get
和 set
操作。
响应式代理可以看这篇文章。
Watcher 和 Dep 可以看这篇文章。
有了这两个概念,接下来重点讲讲渲染和挂载
# Vue 的渲染与挂载
Vue 的 Root 实例初始化完了之后,就会进入渲染与挂载的流程。
# 将 template 模板转换为 render 函数
在这个步骤中,Vue 会将 HTML 通过正则表达式转换为 AST(Abstract Syntax Tree)抽象语法树,然后将 AST 转换为 render 函数。这个过程包括将 HTML 解析为 AST、优化 AST 以提高渲染性能,最后生成 render 函数。
这块代码在编辑版的 vue 是通过 vue-loader 去帮忙转换的,而 browser 版本则会把这段转换器写到 vue.js 中。
Vue 的视图渲染中,最重要的两个函数,也是接下来核心流程。
- render — 虚拟 DOM 的渲染
- update — 真实 DOM 的挂载
# 虚拟 DOM 渲染
因为提前的转换,得到 render 函数。在 Render 函数中,主要是把其中的函数创建成一个一个虚拟 DOM。下面的虚拟 DOM 的部分属性定义
export default class VNode {
tag?: string; // html的标签,例如div, span
data: VNodeData | undefined; // vue相关的数据,例如key, class, style等
children?: Array<VNode> | null; // 孩子数据
text?: string; // 文本标签
elm: Node | undefined; // HTML的Node节点
ns?: string; // 节点的namespace
context?: Component; // 函数化组件的作用域
key: string | number | undefined; // 节点的key属性
componentOptions?: VNodeComponentOptions; // 创建组件实例时会用到的选项信息
componentInstance?: Component; // 组件实例
parent: VNode | undefined | null; // 组件的父亲节点
}
再转换成虚拟 DOM 中,有一些特别的类型会做特殊操作。
- 自定义组件
- slot
# 自定义组件
对于自定义组件,会创建一个 hook,这个 hook 用于初始化组件,在挂载的时候会执行。在定义组件的时候会传入 components
属性,Vue 会给它包装 Vue.extend
函数。 extend
的作用是创建一个 Sub 类,并将其 options
属性跟全局 Vue 的 options
属性给融合起来,并继承所有 Vue 的所有方法,包括创建 vnode
方法和挂载等。
const Sub = function VueComponent(this: any, options: any) {
this._init(options);
} as unknown as typeof Component;
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
Sub.options = mergeOptions(Super.options, extendOptions);
Sub['super'] = Super;
回到 render
方法,如果判断到是自定义组件会执行 installComponentHooks
和把构造器传入 componentOptions
属性, installComponentHooks
会生成 init
的 hooks。这个 init hook
的作用会到挂载的时候说。
function installComponentHooks(data: VNodeData) {
const hooks = data.hook || (data.hook = {});
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i];
const existing = hooks[key];
const toMerge = componentVNodeHooks[key];
// @ts-expect-error
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
}
}
}
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
const child = (vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
));
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
};
# Slot
Vue 初始化阶段,将组件 Slot
当成 children
放到 componentOptions
上作为虚拟节点的属性,并放到组件的 $options._renderChildren
中。
在初始化 render 的时候( initRender
),会将 _renderChildren
做处理,做出一个映射表放到 $slot
属性上,
在 render 阶段的时候结果放到 $scopeSlots
中,而且 $scopeSlots
中都是一个一个函数,对应的 Map
就是 name: fn
。
export function initInternalComponent(
vm: Component,
options: InternalComponentOptions
) {
const opts = (vm.$options = Object.create((vm.constructor as any).options))
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions!
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
**opts._renderChildren = vnodeComponentOptions.children**
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
function normalizeScopedSlot(vm, normalSlots, key, fn) {
const normalized = function () {
const cur = currentInstance
setCurrentInstance(vm)
let res = arguments.length ? fn.apply(null, arguments) : fn({})
res =
res && typeof res === 'object' && !isArray(res)
? [res] // single vnode
: normalizeChildren(res)
const vnode: VNode | null = res && res[0]
setCurrentInstance(cur)
return res &&
(!vnode ||
(res.length === 1 && vnode.isComment && !isAsyncPlaceholder(vnode))) // #9658, #10391
? undefined
: res
}
// this is a slot using the new v-slot syntax without scope. although it is
// compiled as a scoped slot, render fn users would expect it to be present
// on this.$slots because the usage is semantically a normal slot.
if (fn.proxy) {
Object.defineProperty(normalSlots, key, {
get: normalized,
enumerable: true,
configurable: true
})
}
return normalized
}
# Vue update 函数
update 函数的作用就是挂载,将虚拟 DOM 渲染成真实 DOM。
先判断是否首次渲染,如果是,通过递归执行挂载,否则进入更新流程,执行diff 算法。
挂载就是根据虚拟 DOM 的类型去处理,原生 DOM 和文本最后还是会执行浏览器的 createElement
函数。
对于一些特殊类型做特殊操作。
# 自定义组件
在 render 操作的时候,已经把 hook 和构造器传入到虚拟 DOM 里,在 update 函数中,就会执行 init hook。在该 hook 里,会直接执行 $mount
的方法, $mount
的方法最后还会再执行 render 和 update 方法,把这个自定义组件的 Vue 实例化。
# Slots
在 render 操作中,已经把所有的 slot 改造成 $scopeSlots
,最后在 update 函数进行挂载的时候,就执行这些方法就可以了
export function renderSlot(
name: string,
fallbackRender: ((() => Array<VNode>) | Array<VNode>) | null,
props: Record<string, any> | null,
bindObject: object | null
): Array<VNode> | null {
const scopedSlotFn = this.$scopedSlots[name];
let nodes;
if (scopedSlotFn) {
// scoped slot
props = props || {};
if (bindObject) {
if (__DEV__ && !isObject(bindObject)) {
warn('slot v-bind without argument expects an Object', this);
}
props = extend(extend({}, bindObject), props);
}
nodes =
scopedSlotFn(props) ||
(isFunction(fallbackRender) ? fallbackRender() : fallbackRender);
} else {
nodes =
this.$slots[name] ||
(isFunction(fallbackRender) ? fallbackRender() : fallbackRender);
}
const target = props && props.slot;
if (target) {
return this.$createElement('template', { slot: target }, nodes);
} else {
return nodes;
}
}