vue 2.X 项目结构

这篇文章CC会记录一下学习

vue [1]

源码的笔记。基于的源码是

2.6 [2]

的版本。

项目目录结构

下图为源码的目录结构,相关信息可以参见

CONTRIBUTING [3]

文件。

整体目录结构
整体目录结构

主要的业务代码都在src中,其它的目录和一般比较规范的前端JS框架项目类似。

整体架构

从整体架构来看,Vue的代码可以分为下面几个层次:

*  compile层。负责对template的html字符串编译得到AST
*  vdom层。负责将AST转为VNode,并提供基于patch从VNode生成DOM的能力
*  observer层。负责提供双向的数据绑定,例如将VNode与其使用到的data绑定,保证data的更新能变动到VNode上并最终落地DOM
*  instance层。负责提供Vue核心数据结构对象的各个方法,包含其生命周期的管理、options的管理等

层与层之间的关系如下:

层与层之间的关系
层与层之间的关系

每一层都可以单独使用。例如compile实际实现了一个template语法的解析器,vdom提供了一套vue的虚拟DOM实现方式,observer则提供一套基于get和set的数据绑定框架,而instance则基于这些功能将用户定义的data和methods作为observer的数据进行使用,实现了一个MVVM的框架。

核心对象间的关系如下,后文CC会重点介绍:

VNode和其它对象的关系
VNode和其它对象的关系

关键代码

这里CC列一下vue中几个CC个人比较感兴趣的地方的实现。

入口代码和打包

打包从package.json中找入口,对应的build的命令为:

"build": "node scripts/build.js",

--- package.json

从build.js的内容看vue使用了rollup.js来打包自己的代码。build.js里比较重要的代码如下:

let builds = require('./config').getAllBuilds()
......
build(builds)

--- scripts\build.js

其通过config.js获取到本次build需要build哪些target,然后对config中获取的build依次执行build函数。build函数主要就是调用config中获取到的target的build动作,然后进行zip压缩后写入到生成文件中,因此这里的重点是config.js中的内容。CC没有用过rollup.js,但这里的config.js应该是满足rollup.js要求的格式。

config.js的builds常量中包含了各类需要被build的target,例如vue.js的target:

  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

--- scripts\config.js

vue.js的入口在这里为上面代码显示的

web/entry-runtime-with-compiler.js

。下面看下这个js文件的实现。

entry-runtime-with-compiler.js的源码在platform的web目录下,从目录结构中我们知道platform中包含的是平台相关的源码。目前在2.6的源码中,platform下面包含了web和weex两个平台。

在entry-runtime-with-compiler.js中可以看到,其对mount方法进行了重载,另外就是给compile进行了赋值:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
......
Vue.compile = compileToFunctions

--- src\platforms\web\entry-runtime-with-compiler.js

这里的Vue对象依旧是platform下的对象,不是core中的,因此代码还是平台相关的。Vue的导入语句为:

import Vue from './runtime/index'

--- src\platforms\web\entry-runtime-with-compiler.js

而在runtime/index中的Vue才来自于平台无关的Vue核心代码:

import Vue from 'core/index'

--- src\platforms\web\runtime\index.js

现在我们来看下entry-runtime-with-compiler.js和runtime/index.js中平台相关的代码都做了什么事。先看index.js的:

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

--- src\platforms\web\runtime\index.js

这里给Vue的config赋值了一些平台特定的工具方法。接着:

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

--- src\platforms\web\runtime\index.js

这里注册了一些平台特定的directives和components。接着:

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

--- src\platforms\web\runtime\index.js

这里给Vue对象薪资了moune和__patch__两个方法。其中mountComponent来自于core,因此是平台无关的。接着:

// devtools global hook
/* istanbul ignore next */
if (inBrowser) {
  setTimeout(() => {
    if (config.devtools) {
      if (devtools) {
        devtools.emit('init', Vue)
      } else if (
        process.env.NODE_ENV !== 'production' &&
        process.env.NODE_ENV !== 'test'
      ) {
        console[console.info ? 'info' : 'log'](
          'Download the Vue Devtools extension for a better development experience:\n' +
          'https://github.com/vuejs/vue-devtools'
        )
      }
    }
    if (process.env.NODE_ENV !== 'production' &&
      process.env.NODE_ENV !== 'test' &&
      config.productionTip !== false &&
      typeof console !== 'undefined'
    ) {
      console[console.info ? 'info' : 'log'](
        `You are running Vue in development mode.\n` +
        `Make sure to turn on production mode when deploying for production.\n` +
        `See more tips at https://vuejs.org/guide/deployment.html`
      )
    }
  }, 0)
}

--- src\platforms\web\runtime\index.js

这里的代码就是用于判断是否要显示devtools信息。

可以看到platforms/web/runtime/index.js主要做了下面几件事:

*  赋值了几个平台特定的方法
*  加载注册了平台特定的directive和component
*  生成了mount方法
*  判断是否显示devtools信息

而对于entry-runtime-with-compiler.js来说,则是由其名字显示的一样,对mount方法又添加了compiler的动作。所以对于platform来说,其主要做的事情就是runtime/index.js做的几个事情,平台特定代码也就是主要是指一些工具函数以及directive和component的特定代码。在使用vue的时候入口就是new Vue,这里的Vue按照这里的分析是在core/index中,而在core/index中可以看到vue引用自instance/index,因此如果要学习Vue的代码的话,从这里开始学习即可:

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

--- src\core\instance\index.js

数据绑定的实现

来看个官网上简单的数据绑定例子的代码:

<div id="app-6">
  <p>{{ message }}</p>
  <input v-model="message">
</div>

--- https://cn.vuejs.org/v2/guide/

var app6 = new Vue({
  el: '#app-6',
  data: {
    message: 'Hello Vue!'
  }
})

--- https://cn.vuejs.org/v2/guide/

这里CC看源码前有两个想知道的事情,一个是message的变动是怎么更新input的value的,另一个是input的value的变动是怎么更新message的值的。

最底层的原理大家都知道,基于JS提供的getter和setter可以实现这个效果(不过实现这个效果对于JS来说有

好几种方式 [4]

)。现在很多的语言都提供了这种机制,例如python、swift等。不过看下vue对这个机制的封装还是很有趣的。

先来看message的变动是怎么影响到input的吧。在new Vue这个instance后,关键的代码如下:

observe(data, true /* asRootData */)

--- src\core\instance\state.js

这里的data就是new Vue参数中的data。observe后面的true表面这里的data是root的data,可以猜测子组件也是用的这个方法实现数据绑定,但参数传递的应该是false。

observe的功能就是实现数据绑定,代码如下:

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

--- src\core\observer\index.js

这里通过new Observer生成一个Observer实例,这几个对象之间的关系如下:

Observer和data的关系
Observer和data的关系

可以看到vue在这里将data转为了一个Observer,后续这个Observer的任何get和set操作,包括其属性的get和set操作都会被监听并触发绑定的事件。Observer的代码如下:

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

--- src\core\observer\index.js

重点看这里的walk,其会对value的所有属性执行绑定:

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

--- src\core\observer\index.js

defineReactive是数据绑定的关键代码,但实际上它的实现非常简单:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  let childOb = !shallow && observe(val)111
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

--- src\core\observer\index.js

defineReactive对相关对象执行了上面提到的observe方法,可以看到这里没有指定第二个参数。接着通过defineProperty绑定get和set方法。对于get来说,每次有一个使用了此对象的调用时,其都会的调用dep.depend将其加入到依赖中。对于set,每次对这个对象进行新的赋值时,都会通过dep.notify对所有get过此对象的依赖执行通知操作,vue就是通过这个机制实现了从变量到变量依赖的通知机制。

注意这里的dep由于其被get和set引用,因此这里的get和set其实是个闭包,捕获了dep。

在Dep中,订阅Dep的订阅者在类型里叫做Wathcer:

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

--- src\core\observer\dep.js

因此整体的关系图为:

对象关系
对象关系

从图中可以清楚的看到对象与对象之间的关系。

既然知道了绑定的工作方式,那么就来看下template是怎么和这里的data关联起来的,及template是怎么dep到data的。首先从上面的代码可以知道Watcher会订阅Dep,而这个订阅动作从代码看应该发生在getter阶段。注意在getter中有如下的代码:

    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

--- src\core\observer\index.js

这里if Dep.target是关键,如果Dep.target不为空,那么dep.depend()就会设置这个依赖关系。Dep.target从全局的代码看都是null的,而真正对其赋值的阶段,正是发生在Watcher对象中:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

--- src\core\observer\watcher.js

注意这里的pushTarget和popTarget两个动作,其正是将Dep.target这个全局变量分别设置为Watcher自身及重新设置为null。

可以猜测,template中input绑定的message最终会成为一个watcher。其在被获取其值的时候,会调用到data中的message的get动作,此时这个绑定关系就完成了。具体template的实现参见下文。

template

这里看下template的实现,template实现分为两部分,一部分是编译,一部分是生成watcher并和data绑定。

template的parser入口代码如下:

  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }

--- src\compiler\index.js

可以看到这里会编译template的字符串生成AST。parse的实现是一个手写的HTML词法及语法分析器。

parse的结果是一个ast对象。在flow中可以看到起定义:

declare type ASTElement = {
  type: 1;
  tag: string;
  attrsList: Array<ASTAttr>;
  attrsMap: { [key: string]: any };
  rawAttrsMap: { [key: string]: ASTAttr };
  parent: ASTElement | void;
  children: Array<ASTNode>;
  start?: number;
  end?: number;
  processed?: true;
  static?: boolean;
  staticRoot?: boolean;
  staticInFor?: boolean;
  staticProcessed?: boolean;
  hasBindings?: boolean;
  text?: string;
  attrs?: Array<ASTAttr>;
  props?: Array<ASTAttr>;
  plain?: boolean;
  pre?: true;
  ns?: string;
  component?: string;
  inlineTemplate?: true;
  transitionMode?: string | null;
  slotName?: ?string;
  slotTarget?: ?string;
  slotScope?: ?string;
  scopedSlots?: { [name: string]: ASTElement };
  ref?: string;
  refInFor?: boolean;
  if?: string;
  ifProcessed?: boolean;
  elseif?: string;
  else?: true;
  ifConditions?: ASTIfConditions;
  for?: string;
  forProcessed?: boolean;
  key?: string;
  alias?: string;
  iterator1?: string;
  iterator2?: string;
  staticClass?: string;
  classBinding?: string;
  staticStyle?: string;
  styleBinding?: string;
  events?: ASTElementHandlers;
  nativeEvents?: ASTElementHandlers;
  transition?: string | true;
  transitionOnAppear?: boolean;
  model?: {
    value: string;
    callback: string;
    expression: string;
  };
  directives?: Array<ASTDirective>;
  forbidden?: true;
  once?: true;
  onceProcessed?: boolean;
  wrapData?: (code: string) => string;
  wrapListeners?: (code: string) => string;
  // 2.4 ssr optimization
  ssrOptimizability?: number;
  // weex specific
  appendAsTree?: boolean;
  // 2.6 $slot check
  has$Slot?: boolean
};

--- flow\compiler.js

在生成ast后,会调用generate生成最终的结果:

  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

--- src\compiler\index.js

generate的实现如下:

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

--- src\compiler\index.js

这里generate的结果,都会被vue放入到options中:

      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

--- src\platforms\web\entry-runtime-with-compiler.js

对于generate来说,其重点是生成code,因为render的核心就是code的字符串内容。在Vue中,基于虚拟DOM来实现对真实DOM的操作,这里的code可以认为就是一个虚拟DOM。简单的可以认为虚拟DOM就是一个数据结构,这个数据结构最终可以生成真实的DOM。因此这里的数据绑定应该就是将options的data和虚拟DOM进行绑定。

那么这里解析器生成的虚拟DOM是怎么和options中的data绑定的呢?具体代码如下:

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
}

--- src\core\instance\lifecycle.js

这段代码主要就是创建一个Render的Watcher。之前CC提到过,Vue的数据绑定主要是通过订阅Dep的Watcher来获取到通知的,因此可以猜测这里的Render Watcher就是负责监听虚拟DOM的一个Watcher。如果是这样的话这个Watcher的构造方法里应该会进行数据绑定,来看下其实现:

    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)

--- src\core\observer\watcher.js

这里isRenderWatcher为true,因此vm的_watcher就绑定到了这个Watcher上。接着的重要代码为:

    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }

--- src\core\observer\watcher.js

可以看到这个Watcher的getter被赋值了expOrFn。exprOrFn的内容为:

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

--- src\core\instance\lifecycle.js

在Watcher构造方法的最后,其会调用其get方法:

    this.value = this.lazy
      ? undefined
      : this.get()

--- src\core\observer\watcher.js

这里的get在上文提到Dep.Target的时候有提到过,这里再来看一下:

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

--- src\core\observer\watcher.js

可以看到这里首先将Dep.Target设置为该Render Watcher,接着执行this.getter方法,即上面提到的updateComponent。如果不出意外那么Render中的变量的绑定就是发生在这里的。来看下updateComponent的实现:

    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

--- src\core\instance\lifecycle.js

首先来看_render()的实现:

  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots
      )
    }
    // 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 {
      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
      }
    }
    // 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
  }

--- src\core\instance\render.js

其主要用途是生成vnode,即虚拟DOM。而生成这个vnode则是上面提到的options中的render函数。render的具体代码这里CC就不看了,只要知道现在vnode以及生成完毕了,其包含了虚拟DOM的所有内容,同时也包含了template中的所有内容就行。

_update的定义如下:

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

--- src\core\instance\lifecycle.js

可以看到起主要是调用__patch__进行“补丁”动作。这里的__patch__就涉及到虚拟DOM的原理了。如果每次虚拟DOM一被更新就去同步生成DOM的话,性能是无法接受的。因此这里的patch就类似于打补丁一样,打个patch,只把diff出来的差异找出来并运用到DOM中。

也就是说在Render Watcher的get调用中,触发了虚拟DOM的真实生成DOM的动作,自然其涉及到的相关options的data如果被用到的话肯定也会被调用其相关的get方法,此时Render Watcher就订阅了相关data的通知。后续这些data变动时,就会调用其update方法。

值得一提的是对于Watcher来说,其update方法其实是一个调度器。感兴趣的可以看下其代码:

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

--- src\core\observer\watcher.js

这几个对象间的关系如下图所示:

VNode和其它对象的关系
VNode和其它对象的关系
[1] https://cn.vuejs.org/index.html
[2] https://github.com/vuejs/vue/tree/2.6
[3] https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md
[4] https://segmentfault.com/a/1190000003882976
本文微信分享二维码


本文由60 X 60整理编写。
如需转载请注明出处并保留文章所有引用的资料来源。
欢迎关注 本心小晴 微博[微博搜索 本心小晴 或扫描下方二维码]。