vue源码分析(三)

472 阅读5分钟

4.Virtual DOM

4-1 什么是Virtual DOM

Virtual DOM 是vue2.0以后才引入的概念,一个真实的DOM他的操作性能是比较高的,一个div元素的第一层key,就有将近300个属性,Virtual DOM 是用 JavaScript 对象来表示 DOM 信息和结构,也就是在js和DOM之间做了一层缓存,去操作一个VDOM的代价自然是要小于操作一个真实DOM的,而对于真实DOM的任何细微操作都有可能引起页面的 重绘回流,从性能的角度出发,这是我们不愿意看到的情况,所以通过对象模拟出来的DOM结构,我们可以对他先进行操作,最后统一把修改映射到真实DOM上。Vue2.0的Virtual DOM参考自 snabbdom

4-2 Virtual DOM相比真实DOM的优势

  • 虚拟 DOM 不会立刻进行回流与重绘操作

  • 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗

  • 虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部

5.vm._render()

vm._render()最终会返回一个 VNode,传入_update函数

核心部分 vnode = render.call(vm._renderProxy, vm.$createElement)vm._renderProxy来调用render方法,并把vm.$createElement作为参数传入

// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    ...
    let vnode
    ...
    try {
      ...
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      ...
    }
    ...
    return vnode
  }

5-1._renderProxy

_renderProxy在生产环境下为vm实例本身,在development环境下则执行initProxy(vm)

// src/core/instance/init.js
if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
} else {
  vm._renderProxy = vm
}

initProxy其实是在开发模式下,通过es6的 Proxy 的has方法,来判断,访问的该值,是否在vm实例上,如果不在,会在控制台报出相应的警告

// src/core/instance/proxy.js
...
initProxy = function initProxy (vm) {
 /**
   * const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)
   * function isNative (Ctor) {
   *    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
   * }
   */
  // 首先判断浏览器是否存在es6的Proxy,并且Proxy需要是浏览器的内置类型
  if (hasProxy) {
    // determine which proxy handler to use
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
      ? getHandler
      // 执行hasHandler
      : hasHandler
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }
}
...
const hasHandler = {
  has (target, key) {
    //
    const has = key in target
    const isAllowed = allowedGlobals(key) ||
      (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
    if (!has && !isAllowed) {
      if (key in target.$data) warnReservedPrefix(target, key)
      // 如果访问了不存在于vm上的属性,则触发warnNonPresent
      else warnNonPresent(target, key)
    }
    return has || !isAllowed
  }
}
...
// 这是vue一个比较常见的报错提示
// 当使用了一个并没有在data,props或者methods定义的变量,则会报这个警告
const warnNonPresent = (target, key) => {
  warn(
    `Property or method "${key}" is not defined on the instance but ` +
    'referenced during render. Make sure that this property is reactive, ' +
    'either in the data option, or for class-based components, by ' +
    'initializing the property. ' +
    'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
    target
  )
}

5-2.$createElement

vm.$createElementvm._c最终返回createElement函数唯一不同的是,vm._c在调用createElement最后一个参数传入的是false,而vm.$createElement 最后一个参数传入的为true

// src/core/instance/render.js
...
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
// 给tampate编译成的render函数使用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
// 给用户自定义的render函数使用
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...

createElement函数实际上为_createElement函数处理了传入的参数,实际_createElement才是核心的生成vnode函数。

// src/core/vdom/create-element.js
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 如果传入的data是数组,那么在调用_createElement时,data开始每一项参数后移,data置空
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 如果最后一个参数为true,那么调用_createElement的最后一个参数为2,否则为1
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  // 最终真正调用 _createElement
  return _createElement(context, tag, data, children, normalizationType)
}

我们自己手写一个render函数,看看在_createElement中是如何执行的

new Vue({
  el: '#app',
  render(c) {
    return c(
      'div',
      {
        attrs: {
          id: 'app'
        }
      },
      'cherish'
    )
  }
})

_createElement函数主要做了

  • 判断normalizationType,对children进行normalizeChildrensimpleNormalizeChildren
  • 判断tag类型,以及config.isReservedTag(tag)是否是相关原生浏览器保留节点等,决定如何生成相关vnode
  • 返回vnode
// src/core/vdom/create-element.js
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  ...
  // 根据normalizationType来决定对children
  // 进行normalizeChildren还是simpleNormalizeChildren
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    ...
    // 如果tag属性是浏览器的保留原生标签,那么创建相应 VNode
    if (config.isReservedTag(tag)) {
      ...
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } 
    ...
  }
  // 如果tag不为string,则执行创建组件操作
  else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
5-2-1 normalizeChildren vs simpleNormalizeChildren

用户写的render函数,会触发vm.$createElement则调用的为createElement(vm, a, b, c, d, true) 执行 _createElement时,normalizationType = ALWAYS_NORMALIZE,则normalizationType = 2,最终会走到children = normalizeChildren(children)

simpleNormalizeChildren是对children进行了一个数组的铺平操作,但是仅仅铺平一层,而normalizeChildren则是对数组进行一个深度递归的铺平操作

// src/core/vdom/helpers/normalize-children.js
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> {
  // 首先判断children是否是一个一个基本数据类型
  /**
    *  function isPrimitive (value) {
    *      return (
    *        typeof value === 'string' ||
    *        typeof value === 'number' ||
    *        typeof value === 'symbol' ||
    *        typeof value === 'boolean'
    *      )
    *   }
    */
  return isPrimitive(children
  // 如果是,则创建一个TextVNode
    ? [createTextVNode(children)]
    // 否则判断children是否是一个array
    : Array.isArray(children)
      // 如果是array则进行normalizeArrayChildren(children)
      ? normalizeArrayChildren(children)
      : undefined
}

normalizeArrayChildren最终返回一个 Array<VNode>,本次示例,children为一个string 'cherish' 最终调用了createTextVNode( new VNode(undefined, undefined, undefined, String(val))),创建了一个本文节点VNode。

// src/core/vdom/helpers/normalize-children.js
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    // 如果c  children[i]是一个数组,并且length > 0
    // 则递归调用normalizeArrayChildren
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        // 如果这组的第一个节点是一个文本节点,并且上一组的最后一个节点也是文本节点
        // 那么进行一个合并的操作
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    }
    // 对原生节点的处理
    else if (isPrimitive(c)) {
        ...
        res.push(createTextVNode(c))
        ...
    }
    // 其他情况的处理
    else {
      ...
      res.push(c)
      ...
    }
  }
  // 最终返回一个Array<VNode>
  return res
}
5-2-2 VNode

创建出相应的VNode

/ src/core/vdom/create-element.js
	 ...
	 vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
      ...
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

  constructor (
      tag, // "div"
      data, // {attrs: {…}}
      children, // [VNode]
      text, // undefiend
      context // vm
    ) {
      this.tag = tag;
      this.data = data;
      this.children = children;
      this.text = text;
      this.elm = elm;
      this.ns = undefined;
      this.context = context;
     ...
    }
  }

至此,vm._render()通过render.call(vm._renderProxy, vm.$createElement)生成了VNode,并成为了 vm._update(vm._render(), hydrating)的第一个参数

6.vm._update()

在首次没有prevVnode的情况下,vm._update()会触发vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)进行初始化

// src/core/instance/lifecycle.js
 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    ...
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    ...
  }

__patch__的在web下的定义是patch函数,否则是空,这也是为了不同平台下的兼容性所做的处理

// src/platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop

patch函数实际上是createPatchFunction({ nodeOps, modules }),传入了两个参数nodeOpsmodules

// src/platforms/web/runtime/patch.js
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'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

nodeOps中提供了很多辅助函数,用于去操作真实的DOM,modules是一些dom生成相关的class,event,attrs等

src/platforms/web/runtime/node-ops.js
/* @flow */

import { namespaceMap } from 'web/util/index'

export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}

export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

export function createComment (text: string): Comment {
  return document.createComment(text)
}

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}
...

createPatchFunction首先解析出传入的 modulesnodeOps,中间是许多函数,这些辅助函数,去实现了各种各样的逻辑,最终返回了patch这个方法也就是说Vue.prototype.__patch__ === createPatchFunction({ nodeOps, modules }) === patch,那么为什么要绕这么一大圈去定义patch,这里边使用了函数柯里化的思想,将平台差异化的内容,在createPatchFunction调用的时候去处理好,这样对于最终调用patch这个方法的时候,他无需再去写许多的if else这些判断平台相关的内容,这也是一个典型的适配器模式的运用

src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend
  // 初始化每个阶段的钩子
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  ...

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    ...
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

以一个简单例子来分析path为我们做了什么

new Vue({
  el: '#app',
  data() {
    return {
      message: 'likefan'
    }
  },
  render(c) {
    return c(
      'div',
      {
        attrs: {
          id: 'app'
        }
      },
      this.message
    )
  }
})

核心部分 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

// src/core/instance/lifecycle.js
// 首次渲染没有prevVnode
 if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }

在patch中则先把真实DOM(div#App)转化为Vnode,然后触发createElm方法,把虚拟DOM转换为了真实DOM

// src/core/vdom/patch.js
  function emptyNodeAt (elm) {
    // 把传入的Node转化为一个空的VNode,new VNode第5个参数传递的是真实DOM
    // 则最终生成的VNode的elm属性为之前的真实Dom
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
  }
  
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
  	 // oldVnode = vm.$el
     if (isUndef(oldVnode)) {
      ...
    } else {
      // 首次触发 oldVnode 是一个真实element节点,触发else
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
       ...
      } else {
      	 if (isRealElement) {
          ...
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          // 把真实DOM转换为了VNode
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        // oldElm是之前的真实DOM,即之前的vm.$el (div #app)
        const oldElm = oldVnode.elm
        // parentElm 为 oldElm.parentNode 则为body元素
        const parentElm = nodeOps.parentNode(oldElm)
        
        // create new node
        // 把虚拟DOM转换为真实DOM
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        ...
      }
    }
  }

createElm方法通过nodeOps.createElement 生成一个真实DOM,挂载到vnode的elm上,然后触发 createChildren(vnode, children, insertedVnodeQueue),最终挂载到el的parentNode上

// src/core/vdom/patch.js
  function createElm(
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    ...
    const data = vnode.data // {attrs: {…}}
    const children = vnode.children // [VNode]
    const tag = vnode.tag // 'div'
    if (isDef(tag)) {
      // 常见的一个报错,使用未注册的组件
      if (process.env.NODE_ENV !== 'production') {
        ...
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
      // vnode上不存在ns,则触发 nodeOps.createElement(tag, vnode)
      // nodeOps.createElement实际是 通过document.createElement(tagName)
      // 生成一个真实DOM最终返回
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
       ...
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }

createChildren方法处理vnode.children,如果是一个文本节点,则直接插入,如果children是一个数组,那么会循环的调用createElm方法,形成一个递归,直到他的children不再是一个vnode而是一个文本节点,这样会先执行子节点的 insert(parentElm, vnode.elm, refElm),最终执行父节点的insert,(insert根据是否传入了参考节点,使用了insertBefore或appendChild) 这样保证一次性插入,不做多余的真实dom操作,引发频繁的重绘和回流(message先插入div,div再插入body)

// src/core/vdom/patch.js
  function createChildren (vnode, children, insertedVnodeQueue) {
    // 如果children是一个数组
    if (Array.isArray(children)) {
      ...
      // 循环调用createElm方法,把children的每一项作为vnode传入
      // 把vnode.elm 作为 createElm 的第三个参数 parentElm
      for (let i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
      }
    }
    // 否则如果 vnode的text属性是一个基本数据类型
    // 那么往这个真实dom节点上插入这个文本节点
    else if (isPrimitive(vnode.text)) {
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
  }

执行createElm函数后,页面已经渲染出了新添加的元素节点,但是旧的节点还是没有移除,最终通过removeVnodes删除旧的节点

// src/core/vdom/patch.js
...
 // create new node
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

	 ...

      // destroy old node
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
...

回顾一下new Vue之后发生了什么

new Vue -> init -> $mount -> compile(template) -> render -> vnode -> patch -> DOM

下一章分析vue组件化的过程