[关闭]
@frank-shaw 2019-11-30T10:17:31.000000Z 字数 3988 阅读 1169

vue源码阅读(五):虚拟DOM的引入

vue 源码 VNode


接着前文,我们详细研究了数据初始化的过程,也了解了数据更新的几个步骤。现在进入到详细的update过程,这个过程涉及到虚拟DOM与更新DOM操作的patch算法。

虚拟DOM

在现代UI结构设计中(统称MV*框架,V是现代的标记语言),数据驱动已经成为一个核心。而引入虚拟DOM,则是数据驱动的一种实现方式。虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,它们是JS对象,能够描述DOM结构和关系。应用 的各种状态变化会作用于虚拟DOM,最终映射到DOM上。工作流程图如下:

屏幕快照 2019-11-23 下午7.42.48.png-21.6kB

优点

  1. 虚拟DOM轻量、快速:当数据发生变化时,引发虚拟DOM的变化。通过新旧虚拟DOM比对可以得到最小DOM操作量(真实DOM操作非常昂贵),从而提升性能和用户体验。
  2. 跨平台:将虚拟dom更新转换为不同运行时特殊操作实现跨平台(Vue的源码结构中就区分了Web平台与Platform平台)。
  3. 兼容性:还可以加入兼容性代码增强操作的兼容性。

Vue引入虚拟DOM的必要性

Vue 1.x中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大 型项目来说是不可接受的。因此,Vue 2.x选择了中等粒度的解决方案,每一个组件一个Watcher实例, 这样状态变化时只能通知到组件,再通过引入虚拟DOM去进行比对和渲染。

可以说,Vue2.x中引入虚拟DOM是必然的。设计结构发生了变化:组件与Watcher之间一一对应。这就要求必须引入虚拟DOM来应对该变化。

源码

我们先来看看Vue2.x中的虚拟DOM长啥样。它的名字叫VNode:

  1. export default class VNode {
  2. tag: string | void;
  3. data: VNodeData | void;
  4. children: ?Array<VNode>;
  5. text: string | void;
  6. elm: Node | void;
  7. ns: string | void;
  8. context: Component | void; // rendered in this component's scope
  9. key: string | number | void;
  10. componentOptions: VNodeComponentOptions | void;
  11. componentInstance: Component | void; // component instance
  12. parent: VNode | void; // component placeholder node
  13. // strictly internal
  14. raw: boolean; // contains raw HTML? (server only)
  15. isStatic: boolean; // hoisted static node
  16. isRootInsert: boolean; // necessary for enter transition check
  17. isComment: boolean; // empty comment placeholder?
  18. isCloned: boolean; // is a cloned node?
  19. isOnce: boolean; // is a v-once node?
  20. asyncFactory: Function | void; // async component factory function
  21. asyncMeta: Object | void;
  22. isAsyncPlaceholder: boolean;
  23. ssrContext: Object | void;
  24. fnContext: Component | void; // real context vm for functional nodes
  25. fnOptions: ?ComponentOptions; // for SSR caching
  26. devtoolsMeta: ?Object; // used to store functional render context for devtools
  27. fnScopeId: ?string; // functional scope id support

里面的变量很多。可知这是一颗树结构,children里面是Array<VNode>。这是怎么生成的呢?我们回忆之前解读源码中的$mount过程,找一个切入点。

core/instance/lifecycle.js中的mountComponent()开始:

  1. export function mountComponent (
  2. vm: Component,
  3. el: ?Element,
  4. hydrating?: boolean
  5. ): Component {
  6. ...//省略
  7. let updateComponent = () => {
  8. //更新 Component的定义,主要做了两个事情:render(生成vdom)、update(转换vdom为dom)
  9. vm._update(vm._render(), hydrating)
  10. }
  11. new Watcher(vm, updateComponent, noop, {
  12. before () {
  13. if (vm._isMounted && !vm._isDestroyed) {
  14. callHook(vm, 'beforeUpdate')
  15. }
  16. }
  17. }, true /* isRenderWatcher */)
  18. ...//省略
  19. }

看到在new Watcher实例时,与updateComponent创建了关联。重点关注vm._render()

core/instance/render.js内:

  1. import { createElement } from '../vdom/create-element'
  2. export function initRender (vm: Component) {
  3. ...//省略
  4. //编译器使用的render
  5. vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  6. //用户编写的render,典型的柯里化
  7. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  8. ...//省略
  9. }
  10. Vue.prototype._render = function (): VNode {
  11. const vm: Component = this
  12. ...//省略
  13. //从选项中获取render函数
  14. const { render, _parentVnode } = vm.$options
  15. // 最终计算出的虚拟DOM
  16. let vnode
  17. // 执行render函数,传入参数是$createElement (常用的render()方法中的h参数)
  18. let vnode = render.call(vm._renderProxy, vm.$createElement)
  19. ...//省略
  20. return vnode
  21. }

看到了VNode。这个过程执行了render函数,用到了createElement()方法。看来创建VNode的核心在这个方法里面。关联到core/vdom/create-element.js

  1. //返回VNode或者由VNode构成的数组
  2. export function createElement (
  3. context: Component,
  4. tag: any,
  5. data: any,
  6. children: any,
  7. normalizationType: any,
  8. alwaysNormalize: boolean
  9. ): VNode | Array<VNode> {
  10. ...//省略
  11. return _createElement(context, tag, data, children, normalizationType)
  12. }
  13. export function _createElement (
  14. context: Component,
  15. tag?: string | Class<Component> | Function | Object,
  16. data?: VNodeData,
  17. children?: any,
  18. normalizationType?: number
  19. ): VNode | Array<VNode> {
  20. ...//省略
  21. //核心: vnode的生成过程
  22. //传入tag可能是原生的HTML标签,也可能是用户自定义标签
  23. let vnode, ns
  24. if (typeof tag === 'string') {
  25. let Ctor
  26. ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  27. //是原生保留标签,直接创建VNode
  28. if (config.isReservedTag(tag)) {
  29. vnode = new VNode(
  30. config.parsePlatformTagName(tag), data, children,
  31. undefined, undefined, context
  32. )
  33. }else if((!data||!data.pre)&&isDef(Ctor=resolveAsset(context.$options, 'components',tag))) {
  34. // 自定义组件,区别对待,需要先创建组件,再创建VNode
  35. vnode = createComponent(Ctor, data, context, children, tag)
  36. } else {
  37. //
  38. vnode = new VNode(
  39. tag, data, children,
  40. undefined, undefined, context
  41. )
  42. }
  43. } else {
  44. // direct component options / constructor
  45. vnode = createComponent(tag, data, context, children)
  46. }
  47. ...//省略
  48. }

汇总

整个流程串起来,我们看到:render函数通过调用createElement()方法,对不同传入的参数类型进行加工,最终得到了VNode树。流程图如下:
屏幕快照 2019-11-24 上午9.03.12.png-37kB

那么新旧VNode之间如何比较变化,进而以最小代价执行真实DOM的更新呢?我们下节的patch算法将会讲到。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注