@frank-shaw
2019-11-30T10:17:31.000000Z
字数 3988
阅读 1169
vue
源码
VNode
接着前文,我们详细研究了数据初始化的过程,也了解了数据更新的几个步骤。现在进入到详细的update过程,这个过程涉及到虚拟DOM与更新DOM操作的patch算法。
在现代UI结构设计中(统称MV*框架,V是现代的标记语言),数据驱动已经成为一个核心。而引入虚拟DOM,则是数据驱动的一种实现方式。虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用 的各种状态变化会作用于虚拟DOM,最终映射到DOM上。工作流程图如下:
Vue 1.x中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大 型项目来说是不可接受的。因此,Vue 2.x选择了中等粒度的解决方案,每一个组件一个Watcher实例, 这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。
可以说,Vue2.x中引入虚拟DOM是必然的。设计结构发生了变化:组件与Watcher之间一一对应。这就要求必须引入虚拟DOM来应对该变化。
我们先来看看Vue2.x中的虚拟DOM长啥样。它的名字叫VNode:
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
里面的变量很多。可知这是一颗树结构,children
里面是Array<VNode>
。这是怎么生成的呢?我们回忆之前解读源码中的$mount
过程,找一个切入点。
从core/instance/lifecycle.js
中的mountComponent()
开始:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
...//省略
let updateComponent = () => {
//更新 Component的定义,主要做了两个事情:render(生成vdom)、update(转换vdom为dom)
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
...//省略
}
看到在new Watcher实例时,与updateComponent创建了关联。重点关注vm._render()
。
在core/instance/render.js
内:
import { createElement } from '../vdom/create-element'
export function initRender (vm: Component) {
...//省略
//编译器使用的render
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
//用户编写的render,典型的柯里化
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...//省略
}
Vue.prototype._render = function (): VNode {
const vm: Component = this
...//省略
//从选项中获取render函数
const { render, _parentVnode } = vm.$options
// 最终计算出的虚拟DOM
let vnode
// 执行render函数,传入参数是$createElement (常用的render()方法中的h参数)
let vnode = render.call(vm._renderProxy, vm.$createElement)
...//省略
return vnode
}
看到了VNode。这个过程执行了render函数,用到了createElement()
方法。看来创建VNode的核心在这个方法里面。关联到core/vdom/create-element.js
:
//返回VNode或者由VNode构成的数组
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
...//省略
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...//省略
//核心: vnode的生成过程
//传入tag可能是原生的HTML标签,也可能是用户自定义标签
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
//是原生保留标签,直接创建VNode
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}else if((!data||!data.pre)&&isDef(Ctor=resolveAsset(context.$options, 'components',tag))) {
// 自定义组件,区别对待,需要先创建组件,再创建VNode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
//
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
...//省略
}
整个流程串起来,我们看到:render函数通过调用createElement()
方法,对不同传入的参数类型进行加工,最终得到了VNode树。流程图如下:
那么新旧VNode之间如何比较变化,进而以最小代价执行真实DOM的更新呢?我们下节的patch算法将会讲到。