专栏首页前端小课堂Vue源码学习和分析笔记

Vue源码学习和分析笔记

准备工作

前序了解

Flow 静态类型检查工具

类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。

/*@flow*/

function split(str) {
  return str.split(' ');
}

split(22);

类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断。

/*@flow*/


function add(x: number, y: number): number {
  return x - y;
}

add('Hello', 11);

点击了解更多Flow相关特性

源码目录设计

Vue.js 的源码都在 src 目录下,其目录结构如下。

src
├── compiler        # 编译相关 
├── core            # 核心代码 
├── platforms       # 不同平台的支持
├── server          # 服务端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代码

compiler

compiler 目录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 AST 语法树,AST语法树优化,代码生成等功能。

编译的工作可以在构建时做(可以借助 webpack、vue-loader 等插件);也可以在运行时做,使用包含构建功能的 Vue.js。编译是一项耗性能的工作,所以更推荐前者——离线编译。

core

core 目录包含了 Vue.js 的核心代码,包括有内置组件、全局 API 封装,Vue 实例化、Obsever、Virtual DOM、工具函数 Util 等等。

platform

Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 native 客户端上。platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。

server

Vue.js 2.0 支持了服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。注意:这部分代码是跑在服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为一谈。

服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记”混合”为客户端上完全交互的应用程序。

sfc

通常我们开发 Vue.js 都会借助 webpack 构建, 然后通过 .vue 单文件来编写组件。

这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。

shared

Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的。

Vue入口文件

Vue入口文件目录 vue/src/core/instance/index.js

// vue/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

采用的是ES5的写法,并不是ES6的Class写法的优点,是因为:

1、使用混入Mixin的方式传入Vue,为Vue的原型prototype上增加方法。class难以实现这种方法 2、此种方式将代码模块合理划分,将扩展分散到多个模块中去实现,使得代码文件不会过于庞大,便于维护和管理。这个编程技巧以后可以用于代码开发实现中。

通过Mixin增加的原型方法:

// vue/src/core/instance/index.js
initMixin(Vue) // _init
stateMixin(Vue) // $set、$delete、$watch
eventsMixin(Vue) // $on、$once、$off、$emit
lifecycleMixin(Vue) // _update、$forceUpdate、$destroy、
renderMixin(Vue) // $nextTick、_render

initGlobalAPI

在 vue/src/core/index.js 中,调用的initGlobalAPI(Vue),是为Vue增加静态方法的,

在路径 vue/src/core/global-api/ 目录下的文件中,都是给Vue添加的静态方法

比如:

Vue.use // 使用plugin
Vue.extend
Vue.mixin 
Vue.component 
Vue.directive 
Vue.filter

new Vue 做了什么

从入口的文件看来,通过new关键字初始化,调用了

// src/core/instance/index.js

this._init(options)

然后从Mixin增加的原型方法看,initMixin(Vue),调用的是为Vue增加的原型方法_init

// src/core/instance/init.js

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ....
    ....
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    
    ....
    ....
    
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
}

从上面的函数名看来,new vue所做的事情,就像一个流程图一样展开了,分别是

  • 合并配置
  • 初始化生命周期
  • 初始化事件中心
  • 初始化渲染
  • 调用beforeCreate钩子函数
  • init injections and reactivity(这个阶段属性都已注入绑定,而且被$watch变成reactivity,但是$el还是没有生成,也就是DOM没有生成)
  • 初始化state状态(初始化了data、props、computed、watcher)
  • 调用created钩子函数。

在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM。

Vue代码初始化的主线逻辑非常分明,使得逻辑和流程非常清楚,这种编程方法值得学习。

Vue实例挂载

实例挂载主要是$mount方法的实现,在 src/platforms/web/entry-runtime-with-compiler.js & src/platforms/web/runtime/index.js 等文件中都有对Vue.prototype.$mount的定义:

// vue/platforms/web/entry-runtime-with-compiler.js

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) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

$mount方法进来会先进行缓存,之后再进行覆盖重写,再重写的方法里面会调用之前缓存的mount方法,这种做法是因为,多个平台platform的mount方法不同,在入口处进行重写,使后续的多入口能够复用公用定义的mount方法(原先原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定义)。

在$mount方法中,会先判断options中 el 是否存在,再判断 render (有template存在的条件下也需要有render函数),之后再是再判断template,会对template做一定的校验,最后使用 compileToFunctions 将template转化为renderstaticRenderFns.

compileToFunctions编译过程就放在下面文章中再详细解释。

mountComponent方法定义在 src/core/instance/lifecycle.js中,

// src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  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)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

从上面的代码可以看到,mountComponent 核心就是先实例化一个渲染Watcher(字段isRenderWatcher),在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。

Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数,这块儿我们会在之后的章节中介绍。

函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。

因此接下来分析的重点在于:vm._updatem._render

_render

Vue的_render是实例的一个私有方法,定义在 src/core/instance/render.js文件中,返回一个虚拟节点vnode。

// src/core/instance/render.js

 Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

这段函数方法的重点在于render方法的调用,第一种是分为手写的render函数,这种并不常用,比较常用的是template模板,在之前的 mounted 方法的实现时,会将template编译为一个render函数。

其中vm._renderProxy是定义在/src/core/instance/proxy.js文件中,判断如果支持Proxy,如果不支持,返回的是vm,支持的话返回用Proxy代理的vm。

// src/core/instance/proxy.js
initProxy = function initProxy (vm) {
  if (hasProxy) {
    // determine which proxy handler to use
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
      ? getHandler
      : hasHandler
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }
}

其中vm.$createElement也就是在 src/core/instance/render.js文件中:

// src/core/instance/render.js

import { createElement } from '../vdom/create-element'
// 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
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.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

可以从注释中看出: vm._c是template模板编译为render function时使用的; vm.$createElement是用户手写的render function时使用;

这两个函数的支持的参数相同,并且内部都调用了 vdom/create-elementcreateElement方法。

Virtual DOM

在讲_update方法之前,了解下Virtual DOM到底是什么?

Virtual DOM也就是虚拟DOM,是真实数据和页面DOM元素之前的缓冲;数据一变化,并不是立马更新所有视图,而是先更新虚拟DOM,再将虚拟DOM和真实DOM进行对比diff,发生变化的部分再更新到真实DOM中,未发生变化的部分,则不进行更新。

下面是Vue对于VNode的定义:

// vue/src/core/vdom/vnode.js
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?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

实际上 Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom 的实现,如果对Virtual DOM感兴趣的话,可以参考virtual-dom,正如其介绍,

A JavaScript DOM model supporting element creation, diff computation and patch operations for efficient re-rendering

VNode是对真实DOM的抽象描述,主要是由几个关键属性、标签名等数据组成,并不是很复杂,主要复杂的对VNode的create、diff、patch等过程。

createElement是怎么实现的

方法入口

Vue.js通过文件 src/core/vdom/create-element.js 来创建VNode元素:

// src/core/vdom/create-element.js

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
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)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } 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()
  }
}

重点是对于 simpleNormalizeChildrennormalizeChildren 的处理,基本的操作就是将树状结构的children数组打平成一维数组。

normalizeArrayChildren 也就是将createElement的第三个参数,即将children不断遍历打平,不断往res里面push数据,只要是数据Array类型就不断遍历,直到是基础类型TextNode,再进行createTextVNode进行创建。

还有对于组件Component的创建,此处先按下不讲,下文再讲。

// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
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
}

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

function isTextNode (node): boolean {
  return isDef(node) && isDef(node.text) && isFalse(node.isComment)
}

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
    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)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}

_update

_update这一步实际是VNode最终去生成真实DOM的过程。

对于_update方法的定义,在 src/core/instance/lifecycle.js 中:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  // 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)
  }
  restoreActiveInstance()
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

可以看出,主要是对patch方法的调用,分别是首次渲染和数据更新的时候会调用;这次先是分析首次调用时,数据更新的部分会在之后响应式原理的时候再进行分析。 _update的主要目的就是将虚拟DOM渲染生成真实的DOM元素。

patch方法在不同平台的调用是不同的,在浏览器中时,是patch方法,而在非浏览器环境中,比如node后端环境时,是一个noop空函数,主要也是因为只要在浏览器环境时才会有DOM元素。 文件:src/platforms/web/runtime/index.js

import { patch } from './patch'

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

最终 patch 调用的是 src/core/vdom/patch.js 中的 createPatchFunction ,其中有个采用闭包来判断环境的技巧,因为patch方法可能是会在 weex 或者 浏览器端 上调用,如果每次调用都 if else 判断一遍,浪费性能不说,还增加了冗余的判断。于是,它采用了通过闭包判断再返回函数覆盖 patch 的方法,这样环境差异就只会判断一次,进而再次执行的时候,就不会再次判断环境。

export function createPatchFunction (backend) {
    // 环境判断
    // ...
    
    return function patch (oldVnode, vnode, hydrating, removeOnly) {
        // ... 
    }

}

同时,createPatchFunction 内部定义了一系列的辅助方法。

所以从例子来分析:

var app = new Vue({
  el: '#app',
  render: function (createElement) {
    return createElement('div', {
      attrs: {
        id: 'app'
      },
    }, this.message)
  },
  data: {
    message: 'Hello Vue!'
  }
})

然后我们在 vm._update 的方法里是这么调用 patch 方法的:

// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

结合例子,在首次渲染时,所以在执行 patch 函数的时候,传入的 vm.$el 对应的是例子中 id 为 app 的 DOM 对象,这个也就是 <div id="app">, vm.$el 的赋值是在之前 mountComponent 函数做的,vnode 对应的是调用 render 函数的返回值,hydrating 在非服务端渲染情况下为 false,removeOnly 为 false。

function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // 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)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

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

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

由于我们传入的 oldVnode 实际上是一个 DOM container,所以 isRealElement 为 true,接下来又通过 emptyNodeAt 方法把 oldVnode 转换成 VNode 对象,然后再调用 createElm 方法。

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      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.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // in Weex, the default insertion order is parent-first.
      // List items can be optimized to use children-first insertion
      // with append="tree".
      const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
      if (!appendAsTree) {
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
      createChildren(vnode, children, insertedVnodeQueue)
      if (appendAsTree) {
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
    } 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)
  }
}

createElm 的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。createComponent 方法目的是尝试创建子组件,接下来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签;然后再去调用平台 DOM 的操作去创建一个占位符元素。

vnode.elm = vnode.ns
  ? nodeOps.createElementNS(vnode.ns, tag)
  : nodeOps.createElement(tag, vnode)

接下来是通过 createChildren 创建子元素:

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,这是一种常用的深度优先的遍历算法,这里要注意的一点是在遍历过程中会把 vnode.elm 作为父容器的 DOM 节点占位符传入。 最后调用 insert 方法把 DOM 插入到父节点中,因为是递归调用,子元素会优先调用 insert,所以整个 vnode 树节点的插入顺序是先子后父。来看一下 insert 方法,它的定义在 src/core/vdom/patch.js 上。

insert(parentElm, vnode.elm, refElm)

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)
    }
  }
}

insert 逻辑很简单,调用一些 nodeOps 把子节点插入到父节点中,这些辅助方法定义在 src/platforms/web/runtime/node-ops.js 中:

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

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

其实就是调用原生 DOM 的 API 进行 DOM 操作。 在 createElm 过程中,如果 vnode 节点不包含 tag,则它有可能是一个注释或者纯文本节点,可以直接插入到父元素中。在我们这个例子中,最内层就是一个文本 vnode,它的 text 值取的就是之前的this.message` 的值 Hello Vue!。

再回到 patch 方法,首次渲染我们调用了 createElm 方法,这里传入的 parentElm 是 oldVnode.elm 的父元素,在我们的例子是 id 为 #app div 的父元素,也就是 Body;实际上整个过程就是递归创建了一个完整的 DOM 树并插入到 Body 上。

最后,我们根据之前递归 createElm 生成的 vnode 插入顺序队列,执行相关的 insert 钩子函数。

这里只是分析了最简单的场景,在实际的项目中,会比这些复杂的很多。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【Rust日报】 2019-08-12 - 哪些软件应用值得用Rust重写?

    Rust安全、高性能等优点吸引到很多优秀的开发者和公司将原有项目的部分或全部用它重写,下面是知乎上关于该问题的回答,如果你有重写的经验和想法,欢迎分享。

    MikeLoveRust
  • 「React Hook」160行代码实现动态炫酷的可视化图表 - 排行榜

    这是一个国外大佬在其公司峰会的代码竞赛中写的一个库:react-dynamic-charts,用于根据动态数据创建动态图表可视化。

    前端劝退师
  • Java 数组及数组常用算法

      数组的定义可以采用两种方法,推荐采用第一种,这样变量的类型是数组这一概念更加直接。

    Carlos Ouyang
  • 前端语言基础【第二篇:JavaScript】

    在js里面需要获取到input里面的值,如果把script标签放到head 里面会出现问题。

    BWH_Steven
  • 【Rust日报】 2019-08-16 - Rust如何走进Facebook

    Chaos: 今天刚看到Facebook内部meetup流出的Slides,原文标题很有意思:Bringing Rust Home to Meet the Pa...

    MikeLoveRust
  • [译] 避免那些可恶的cannot read property of undefined 错误

    Uncaught TypeError: Cannot read property 'foo' of undefined. 是一个我们在 JavaScript 开...

    ConardLi
  • require时,exports和module.exports的区别你真的懂吗?

    提到 exports 和 module.exports 我们不得不提到 require 关键字。大家哦读知道 Node.js 遵循 CommonJS 规范,使用...

    coder_koala
  • 微信小程序知识云开发

    小程序界面设计、交互、功能与他人的手机应用软件或在先发布的小程序构成实质性相似,构成小程序抄袭

    达达前端
  • 56 道高频 JavaScript 与 ES6+ 的面试题及答案

    用正则表达式匹配字符串,以字母开头,后面是数字、字符串或者下划线,长度为 9 - 20

    夜尽天明
  • Java8 CompletableFuture 编程

     所谓异步调用其实就是实现一个无需等待被调用函数的返回值而让操作继续运行的方法。在 Java 语言中,简单的讲就是另启一个线程来完成调用中的部分计算,使调用继续...

    JMCui

扫码关注云+社区

领取腾讯云代金券