前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue2.5源码阅读笔记02—虚拟DOM的创建与渲染

Vue2.5源码阅读笔记02—虚拟DOM的创建与渲染

原创
作者头像
CS逍遥剑仙
修改2018-09-11 11:56:47
1.8K0
修改2018-09-11 11:56:47
举报
文章被收录于专栏:禅林阆苑

Vue2.5源码阅读笔记02—虚拟DOM的创建与渲染

Write By CS逍遥剑仙

我的主页: www.csxiaoyao.com

GitHub: github.com/csxiaoyaojianxian

Email: sunjianfeng@csxiaoyao.com

QQ: 1724338257

1. 数据驱动与虚拟DOM

Vue是数据驱动的MVVM框架,视图是由数据驱动生成的,因此对视图的修改不是通过操作 DOM,而是通过修改数据,相比传统使用jQuery的前端开发,能够大大简化代码量,尤其在交互逻辑复杂的情况下,减少DOM操作,直接操作数据会让代码的逻辑变的非常清晰、利于维护。

真实DOM存储的节点信息非常多,频繁的DOM操作会带来明显的性能问题,虚拟DOM能有效解决性能问题。在Vue中,虚拟DOM由Vue中$mount实例方法调用mountComponent 函数生成,vm._render负责创建虚拟DOM,vm._update负责渲染虚拟DOM。

2. 虚拟DOM渲染流程

虚拟DOM的渲染是按照下面的流程运行的,后面会详细介绍。

(1) new Vue ==> (2) init ==> (3) $mount ==> (4) compile ==> (5) render ==> (6) vnode ==> (7) patch ==> (8) DOM

3. Vue实例挂载

Vue通过 $mount 实例方法挂载 vm$mount 方法的实现和平台、构建方式都相关,因此在项目中有多处实现,其中,带 compiler 版本的 $mount 可以在浏览器中使用,有利于对源码进行调试分析,作为学习,应该从带 compiler 的版本入手,具体的实现在 src/platform/web/entry-runtime-with-compiler.js 中。

代码语言:txt
复制
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    ...
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
	...
  }
  return mount.call(this, el, hydrating)
}

首先对原型上的 $mount 方法进行缓存,目的是为了重新定义该方法,以便在 $mount 方法执行前执行平台差异的代码。以web平台为例,传入两个参数:elhydrating(服务端渲染相关,此处无需传入),重新定义的$mount执行了一些平台相关的额外操作,首先限制 el 不能为 bodyhtml 这类根节点,接着,检查是否有 render 方法,如果没有则会把 el 或者 template 字符串转换成 render 方法,最后调用 compileToFunctions 方法实现render在线编译。

原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定义,$mount 方法实际会调用定义在 src/core/instance/lifecycle.js 中的 mountComponent 方法进行挂载。

代码语言:txt
复制
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    ...
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

mountComponent 先调用 vm._render 方法先生成虚拟 Node,再实例化一个Watcher,由此看出,渲染最核心的 2 个方法:vm._rendervm._update

4. vm._render创建VDOM

Vue 的 _render 方法是实例的一个私有方法,可以把实例渲染成一个虚拟 Node,定义在 src/core/instance/render.js 中。平时开发工作中很少手写 render ,大多是写 template 模板,在上面的 mounted 方法中会把 template 编译成 render 方法。VDOM是由VNODE组成的树形结构,_render 函数中创建VNODE的实现是通过调用 createElement方法,定义在 src/core/vdom/create-elemenet.js 中

4.1 createElement创建VNODE

Virtual DOM 的节点定义的描述在 src/core/vdom/vnode.js 中,vnode.js 详细描述了VNODE的结构,比真实DOM结构简化了很多,Vue 的 Virtual DOM 是借鉴了开源库 snabbdom 的实现。除自身的数据结构的定义,映射到真实 DOM 要经历 VNode 的 create、diff、patch 等过程。

VNode 的创建通过 createElement 方法创建,定义在 src/core/vdom/create-elemenet.js 中。

代码语言:txt
复制
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

createElement 方法的最后调用了 _createElement 私有方法。

代码语言:txt
复制
exporte  function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  ...    
}

_createElement 方法有 5 个参数,context 表示 VNode 的上下文环境;tag 表示标签;data 表示 VNode 的数据,它是一个 VNodeData 类型,定义在 flow/vnode.js 中;children 表示当前 VNode 的子节点,将会被规范为标准的 VNode 数组;normalizationType 表示子节点规范的类型,类型不同规范的方法不同,由 render 函数是编译生成的还是用户手写决定。createElement 中最关键的两个流程是 normalizeChildren 和 VNODE 创建。

4.2 normalizeChildren子节点规范化

Virtual DOM 是树状结构,每一个 VNode 可能会有若干个子节点,并且这些子节点也为 VNode 类型,因此需要在 createElement 过程中将传入的 any 类型的 children 参数规范化为 VNODE。

_createElement 会根据传入的 normalizationType 参数的不同,分别调用 normalizeChildren(children)simpleNormalizeChildren(children) 方法,二者都定义在 src/core/vdom/helpers/normalzie-children.js 中。simpleNormalizeChildren 是当render 由函数是编译生成时调用,大部分编译生成的 children 已是 VNode 类型的,除了 functional component 函数式组件返回的是一个数组而不是一个根节点,所以需要通过 Array.prototype.concat 方法把 children 数组变成深度只有一层的一维数组。normalizeChildren 方法存在两种调用场景,一是 render 函数由用户手写,当 children只有一个节点时,Vue调用 createTextVNode 创建一个文本节点的 VNode;另一场景是当编译 slotv-for 的时候会产生嵌套数组的情况,会调用 normalizeArrayChildren 方法进行处理。

代码语言:txt
复制
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

4.3 VNODE创建

规范化 children 后便可以创建 VNode 的实例。如果是内置节点,则直接创建普通 VNode,如果是为已注册的组件名,则通过 createComponent 创建一个组件类型的 VNode,否则创建一个未知的标签的 VNode。

代码语言:txt
复制
let vnode, ns
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  vnode = createComponent(tag, data, context, children)
}

5. vm._update渲染VDOM

Vue 的 _update 是实例的私有方法,它只在首次渲染和数据更新两种情况下被调用,_update 方法把 VNode 渲染成真实的 DOM,定义在 src/core/instance/lifecycle.js 中。

代码语言:txt
复制
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  if (!prevVnode) {
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
}

_update 的核心是调用 vm.__patch__ 方法,定义在 src/platforms/web/runtime/index.js 中,不同平台的定义不同,浏览器端渲染的 patch 方法定义在 src/platforms/web/runtime/patch.js中。

代码语言:txt
复制
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })

patch 方法的定义是调用 createPatchFunction 方法的返回值,传入 nodeOps 参数和 modules 参数。其中,nodeOps 封装了一系列 DOM 操作方法,modules 定义了一些模块的钩子函数的实现。

代码语言:txt
复制
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

首次渲染执行 patch 函数的时候,传入的 vm.$el 是例子中形如<div id="app">的 DOM 对象, vm.$el 的赋值在之前 mountComponent 函数中完成,vnode 是调用 render 函数的返回值,hydrating 在非服务端渲染时为 false,removeOnly 为 false。patch的关键操作如下:

代码语言:txt
复制
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
  if (isRealElement) {
    if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
      oldVnode.removeAttribute(SSR_ATTR)
      hydrating = true
    }
    ...  
    oldVnode = emptyNodeAt(oldVnode)
  }
  const oldElm = oldVnode.elm
  const parentElm = nodeOps.parentNode(oldElm)
  createElm(
    vnode,
    insertedVnodeQueue,
    oldElm._leaveCb ? null : parentElm,
    nodeOps.nextSibling(oldElm)
  )
}

emptyNodeAt 方法把 oldVnode 转换成 VNode 对象,然后再调用 createElm 方法。createElm 的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。 对于创建真实DOM子元素,调用了createChildren方法。

代码语言:txt
复制
function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

createChildren 遍历子虚拟节点,递归调用 createElm实现深度优先遍历。接着再调用 invokeCreateHooks 方法执行所有的 create 的钩子并把 vnode push 到 insertedVnodeQueue 队列中。

代码语言:txt
复制
function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

最后调用 insert 方法把 DOM 插入到父节点中,因为是递归调用,子元素会优先调用 insertinsert方法定义在 src/core/vdom/patch.js 上,最终使用原生DOM操作进行了渲染,实际上整个过程就是递归创建了一个完整的 DOM 树并插入到 Body 上。在 createElm 过程中,如果 vnode 节点不包含 tag,可能是注释或者纯文本节点,可以直接插入到父元素中。

代码语言:txt
复制
function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (ref.parentNode === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

至此,虚拟DOM渲染为真实DOM。

www.csxiaoyao.com
www.csxiaoyao.com

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Vue2.5源码阅读笔记02—虚拟DOM的创建与渲染
    • 1. 数据驱动与虚拟DOM
      • 2. 虚拟DOM渲染流程
        • 3. Vue实例挂载
          • 4. vm._render创建VDOM
            • 4.1 createElement创建VNODE
            • 4.2 normalizeChildren子节点规范化
            • 4.3 VNODE创建
          • 5. vm._update渲染VDOM
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档