GithubHelp home page GithubHelp logo

vue2.0-source's People

Contributors

changjin0520 avatar fuuuoverclocking avatar jiangtao avatar liutao avatar meixg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

vue2.0-source's Issues

从头说起

我们学习阅读一个项目的源码时,首先当然要看它的package.json文件。这里面有项目的依赖,有开发环境、生产环境等编译的启动脚本,有项目的许可信息等。别的暂且不说,直接来看script。我们发现Vue.js有许许多多的npm命令。这里我们只看第一个,也就是npm run dev所执行的命令。

"dev": "rollup -w -c build/config.js --environment TARGET:web-full-dev"

我们发现这里用到了rollup,感兴趣的可以去了解一下,这里它并不是重点,我们只需要知道它是一个类似于webpack的打包工具就行了。往后看我们发现它执行了build/config.js,前面说过它是一个打包配置的文件,我们看看它里面又做了哪些事儿。

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

function genConfig (opts) {
  ...
}

if (process.env.TARGET) {
  module.exports = genConfig(builds[process.env.TARGET])
} else {
  exports.getBuild = name => genConfig(builds[name])
  exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name]))
}

直接看下面,我们看到它调用了getConfig(builds[process.env.TARGET])getConfig用于生成rollup的配置文件。builds是一个对象,获取它的process.env.TARGET值,在package.json中,我们看到dev中有TARGET:web-full-dev参数,即上面我留下的那一段配置。这样入口文件我们就找到了,也就是/src/entries/web-runtime-with-compiler.js

既然我们找到了入口,我们就从这里开始我们的Vue.js源码之旅。

打开web-runtime-with-compiler.js文件,在该文件的第一行,我们看到如下代码:

import Vue from './web-runtime'

即引入了同一目录下的另一文件,本文件中我们只不过在'./web-runtime'导出的Vue对象上进行了二次加工而已。

打开web-runtime,看第一行代码我们就知道,该文件同理是在'core/index'导出的Vue对象上进行了加工。

再次打开core/index,发现它又是在'./instance/index'上进行加工的。这也是为什么打包后的文件内,最终返回的是Vue$3。整个过程是这样的:

/src/entries/web-runtime-with-compiler.js    -->    /src/entries/web-runtime.js    --> 
   /src/core/index.js    -->    /src/core/instance/index.js

历经千辛万苦,终于找到了定义Vue对象的所在之处。它的构造函数及其简单:

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

首先判断如果是生产环境,且不是通过new关键字来创建对象的话,就在控制台打印一个warning,之后调用了this._init(options)函数。

下面的几个函数,分别在Vue.prototype原型上绑定了一些实例方法。关于Vue静态方法实例方法,我分别单列出来,这样看起来可以更加清晰。

// _init
initMixin(Vue)  
// $set、$delete、$watch
stateMixin(Vue)
// $on、$once、$off、$emit
eventsMixin(Vue)
// _update、$forceUpdate、$destroy
lifecycleMixin(Vue)
// $nextTick、_render、以及多个内部调用的方法
renderMixin(Vue)

我们沿着刚才所提到的文件引入顺序一步步来看。 /src/core/instance/index.js执行之后,是/src/core/index.js文件。

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Vue.version = '__VERSION__'

该文件也很简单,首先调用了initGlobalAPI,引自/src/core/global-api/index

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  Vue.options = Object.create(null)
  // Vue.options.components、Vue.options.directives、Vue.options.filters
  config._assetTypes.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  // Vue.options._base
  Vue.options._base = Vue

  // Vue.options.components.KeepAlive
  extend(Vue.options.components, builtInComponents)

  // Vue.use
  initUse(Vue)
  // Vue.mixin
  initMixin(Vue)
  // Vue.extend
  initExtend(Vue)
  // Vue.component、Vue.directive、Vue.filter
  initAssetRegisters(Vue)
}

从上面的代码可以看出,它是给Vue对象添加了一些静态方法和属性。我们之后在Vue的静态方法中详细分析。

/src/core/index.js文件中,还添加了一个Vue.prototype[$isServer]属性,用于判断是不是服务端渲染,还有一个就是Vue.version

接着是/src/entries/web-runtime.js

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

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

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

Vue代码整体上可以分为两个平台,一个是我们常用的web,另一个是weex。所以源码里把两个平台不同的内容单独提取出来了。这里我们只谈web

首先,在Vue.config上添加了几个平台相关的方法,扩展了Vue.options.directivesmodelshow)和Vue.options.componentsTransitionTransitionGroup)。在Vue.prototype上添加了__patch__(虚拟dom相关)和$mount(挂载元素)。

最后是/src/entries/web-runtime-with-compiler.js,该文件主要干了两件事,一个是定义了一个方法Vue.prototype.$mount,另一个是将compileToFunctions挂在到Vue.compile上。

以上,简单且有点儿啰嗦的大体上讲了一下Vue源码的结构,接下来,我们从一个小栗子入手。看看Vue从创建对象,到挂载,到修改都分别经历了什么。

Vue的静态方法

该文件主要是为了方便列出直接绑定在Vue上的静态方法和静态属性都有哪些,以及绑定的位置,每个方法的作用是什么。

// src/core/index.js
Vue.version = '__VERSION__'

// src/entries/web-runtime-with-compiler.js
Vue.compile = compileToFunctions    // 把模板template转换为render函数

// src/core/global-api 在目录结构中,我们指出,Vue的静态方法大多都是在该文件夹中定义的
// src/core/global-api/index.js
Vue.config //不过以直接替换整个config对象
Vue.util //几个工具方法,但是官方不建议使用
Vue.set
Vue.delete
Vue.nextTick
Vue.options = {
  components: {KeepAlive: KeepAlive}
  directives: {},
  filters: {},
  _base: Vue
}

// src/core/global-api/use.js
Vue.use

// src/core/global-api/mixin.js
Vue.mixin

// src/core/global-api/extend.js
Vue.extend

// src/core/global-api/assets.js
Vue.component
Vue.directive
Vue.filter

我的期望是有时间每个方法的具体实现都进行分析,慢慢来。

从一个小栗子看Vue的生命周期

既然是源码分析,所以大家最好对着源码,一步一步来看。本篇文章,旨在通过一个简单的小栗子,带着大家从vm创建,到显示到页面上都经历了哪些过程。

例子如下:

<div id="app">
	<p>{{message}}</p>
</div>
<script type="text/javascript">
	var vm = new Vue({
		el: '#app',
		data: {
			message: '第一个vue实例'
		}
	})
</script>

创建对象,当然要从构造函数看起,构造函数在src/core/instance/index.js中。

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

我们看到,它首先判断了是不是通过new关键词创建,然后调用了this._init(options)_init函数是在src/core/instance/init.js中添加的。我们先把整个函数都拿出来,然后看看每一步都做了什么。

this._init

 Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    // 性能统计相关
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-init:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // 内部使用Vnode部分使用
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // 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 (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }

首先一进来,我们给当前vm添加了一个唯一的_uid,然后vm._isVue设为true(监听对象变化时用于过滤vm)。

因为我们自己传入的参数中,理论上不会有_isComponent。所以我们的小栗子就直接走到了else里面。mergeOptions用于合并两个对象,不同于Object.assign的简单合并,它还对数据还进行了一系列的操作,且源码中多处用到该方法,所以后面会详细讲解这个方法。在这之前,我们先看看resolveConstructorOptions都做了什么。

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  // 有super属性,说明Ctor是通过Vue.extend()方法创建的子类
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

这里的Ctor就是vm.constructor也就是Vue对象,在上一篇文章中,其实我们提到过,在/src/core/global-api/index文件中,我们给Vue添加了一些全局的属性或方法。

  Vue.options = Object.create(null)
  // Vue.options.components、Vue.options.directives、Vue.options.filters
  config._assetTypes.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // Vue.options._base
  Vue.options._base = Vue

  // Vue.options.components.KeepAlive
  extend(Vue.options.components, builtInComponents)

所以,这里打印一下Ctor.options,如下所示:

Ctor.options = {
	components: {
		KeepAlive,
		Transition,
		TransitionGroup
	},
	directives: {
		model,
		show
	},
	filters: {},
	_base: Vue
}

Ctor.super是在调用Vue.extend时,才会添加的属性,这里先直接跳过。所以mergeOptions的第一个参数就是上面的Ctor.options,第二个参数是我们传入的options,第三个参数是当前对象vm

mergeOptions

本来打算在此处详细讲解一下mergeOptions,但写着写着发现内容太多。有点偏离主题,所以这里还是基于前面的例子,简单说一下,另起一篇文章详细讲解Vue的合并策略。

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
  	// 如果有options.components,则判断是否组件名是否合法
    checkComponents(child)
  }
  // 格式化child的props
  normalizeProps(child)
  // 格式化child的directives
  normalizeDirectives(child)
  // options.extends
  const extendsFrom = child.extends 
  if (extendsFrom) {
    parent = typeof extendsFrom === 'function'
      ? mergeOptions(parent, extendsFrom.options, vm)
      : mergeOptions(parent, extendsFrom, vm)
  }
  // options.mixins
  if (child.mixins) { 
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      let mixin = child.mixins[i]
      if (mixin.prototype instanceof Vue) {
        mixin = mixin.options
      }
      parent = mergeOptions(parent, mixin, vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

前面和componentspropsdirectivesextendsmixins相关的内容我们暂且忽略,我们知道Vue提供了配置optionMergeStrategies对象,来让我们手动去控制属性的合并策略,这里的strats[key]就是key属性的合并方法。

function mergeAssets (parentVal: ?Object, childVal: ?Object): Object {
  const res = Object.create(parentVal || null)
  return childVal
    ? extend(res, childVal)
    : res
}

config._assetTypes.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

_assetTypes就是componentsdirectivesfilters,这三个的合并策略都一样,这里我们都返回了parentVal的一个子对象。

data属性的合并策略,是也是Vue内置的,如下:

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal
  const keys = Object.keys(from)
  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    if (!childVal) {
      return parentVal
    }
    if (typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    return function mergedDataFn () {
      return mergeData(
        childVal.call(this),
        parentVal.call(this)
      )
    }
  } else if (parentVal || childVal) {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm)
        : undefined
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

这里vmdata都不为空,所以会走到else if,返回的是mergedInstanceDataFn方法。关于mergedInstanceDataFn方法,我们都知道,子组件中定义data时,必须是一个函数,这里简单的判断了是函数就执行,不是就返回自身的值。然后通过mergeData去合并,其实就是递归把defaultData合并到instanceData,并观察。

最后合并之后的vm.$option如下:

vm.$option = {
	components: {
		KeepAlive,
		Transition,
		TransitionGroup
	},
	directives: {
		model,
		show
	},
	filters: {},
	_base: Vue,
	el: '#app',
	data: function mergedInstanceDataFn(){}
}

回到我们的_init接着放下看,之后如果是开发环境,则vm._renderProxy值为一个Proxy代理对象,生产环境就是vm自身,这里不展开赘述。

接着就是一系列的操作,我们一个一个来看。

initLifecycle(vm)

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

该方法比较简单,主要就是给vm对象添加了$parent$root$children属性,以及一些其它的声明周期相关的标识。

options.abstract用于判断是否是抽象组件,组件的父子关系建立会跳过抽象组件,抽象组件比如keep-alivetransition等。所有的子组件$root都指向顶级组件。

initEvents(vm)

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

该方法给vm添加了一些事件相关的属性,我们本例中没有添加事件,暂时跳过。

initRender(vm)

export function initRender (vm: Component) {
  vm.$vnode = null 
  vm._vnode = null 
  vm._staticTrees = null
  const parentVnode = vm.$options._parentVnode
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject

  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

这里给vm添加了一些虚拟dom、slot等相关的属性和方法。

然后会调用beforeCreate钩子函数。

initInjections(vm)initProvide(vm)

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

export function initInjections (vm: Component) {
  const inject: any = vm.$options.inject
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    // isArray here
    const isArray = Array.isArray(inject)
    const keys = isArray
      ? inject
      : hasSymbol
        ? Reflect.ownKeys(inject)
        : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      const provideKey = isArray ? key : inject[key]
      let source = vm
      while (source) {
        if (source._provided && provideKey in source._provided) {
          if (process.env.NODE_ENV !== 'production') {
            defineReactive(vm, key, source._provided[provideKey], () => {
              warn(
                `Avoid mutating an injected value directly since the changes will be ` +
                `overwritten whenever the provided component re-renders. ` +
                `injection being mutated: "${key}"`,
                vm
              )
            })
          } else {
            defineReactive(vm, key, source._provided[provideKey])
          }
          break
        }
        source = source.$parent
      }
    }
  }
}

这两个配套使用,用于将父组件_provided中定义的值,通过inject注入到子组件,且这些属性不会被观察。简单的例子如下:

<div id="app">
	<p>{{message}}</p>
	<child></child>
</div>
<script type="text/javascript">
	var vm = new Vue({
		el: '#app',
		data: {
			message: '第一个vue实例'
		},
		components: {
			child: {
				template: "<div>{{a}}</div>",
				inject: ['a']
			}
		},
		provide: {
			a: 'a'
		}
	})
</script>

initState(vm)

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch) initWatch(vm, opts.watch)
}

这里主要就是操作数据了,propsmethodsdatacomputedwatch,从这里开始就涉及到了ObserverDepWatcher,网上讲解双向绑定的文章很多,之后我也会单独去讲解这一块。而且,这里对数据操作也比较多,在讲完双向绑定的内容后,我们再结合没讲完的mergeOptions详细看看Vue对我们传入的数据都进行了什么操作。

到这一步,我们看看我们的vm对象变成了什么样:

// _init
vm._uid = 0
vm._isVue = true
vm.$options = {
    components: {
		KeepAlive,
		Transition,
		TransitionGroup
	},
	directives: {
		model,
		show
	},
	filters: {},
	_base: Vue,
	el: '#app',
	data: function mergedInstanceDataFn(){}
}
vm._renderProxy = vm
vm._self = vm

// initLifecycle
vm.$parent = parent
vm.$root = parent ? parent.$root : vm

vm.$children = []
vm.$refs = {}

vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false

// initEvents	
vm._events = Object.create(null)
vm._hasHookEvent = false

// initRender
vm.$vnode = null
vm._vnode = null
vm._staticTrees = null
vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// 在 initState 中添加的属性
vm._watchers = []
vm._data
vm.message

然后,就会调用我们的created钩子函数。

我们看到create阶段,基本就是对传入数据的格式化、数据的双向绑定、以及一些属性的初始化。

$mount

打开src/entries/web-runtime-with-compiler.js

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  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) {

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}

function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

首先,通过mount = Vue.prototype.$mount保存之前定义的$mount方法,然后重写。

这里的query可以理解为document.querySelector,只不过内部判断了一下el是不是字符串,不是的话就直接返回,所以我们的el也可以直接传入dom元素。

之后判断是否有render函数,如果有就不做处理直接执行mount.call(this, el, hydrating)。如果没有render函数,则获取templatetemplate可以是#id、模板字符串、dom元素,如果没有template,则获取el以及其子内容作为模板。

compileToFunctions是对我们最后生成的模板进行解析,生成render。这里的内容也比较多,简单说一下:

该方法创建的地方在src/compiler/index.jscreateCompiler中。

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  optimize(ast, options)
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}


export function createCompiler (baseOptions: CompilerOptions) {
  const functionCompileCache: {
    [key: string]: CompiledFunctionResult;
  } = Object.create(null)

  function compile (
    template: string,
    options?: CompilerOptions
  ): CompiledResult {

  	...

    const compiled = baseCompile(template, finalOptions)
    
    ...

    return compiled
  }

  function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = options || {}

    ...

    // compile
    const compiled = compile(template, options)

    ...

    return (functionCompileCache[key] = res)
  }

  return {
    compile,
    compileToFunctions
  }
}

compileToFunctions中调用了compilecompile中调用了baseCompile。主要的操作就是baseCompile中的三步。

第一步, const ast = parse(template.trim(), options)。这里是解析template,生成ast。我们的例子生成的ast如下:

{
	type: 1,
	tag: 'div',
	plain: false,
	parent: undefined,
	attrs: [{name:'id', value: '"app"'}],
	attrsList: [{name:'id', value: 'app'}],
	attrsMap: {id: 'app'},
	children: [{
		type: 1,
		tag: 'p',
		plain: true,
		parent: ast,
		attrs: [],
		attrsList: [],
		attrsMap: {},
		children: [{
			expression: "_s(message)",
			text: "{{message}}",
			type: 2
	}]
}

第二步,optimize(ast, options)主要是对ast进行优化,分析出静态不变的内容部分,增加了部分属性:

{
	type: 1,
	tag: 'div',
	plain: false,
	parent: undefined,
	attrs: [{name:'id', value: '"app"'}],
	attrsList: [{name:'id', value: 'app'}],
	attrsMap: {id: 'app'},
	static: false,
	staticRoot: false,
	children: [{
		type: 1,
		tag: 'p',
		plain: true,
		parent: ast,
		attrs: [],
		attrsList: [],
		attrsMap: {},
		static: false,
		staticRoot: false,
		children: [{
			expression: "_s(message)",
			text: "{{message}}",
			type: 2,
			static: false
	}]
}

因为我们这里只有一个动态的{{message}},所以staticstaticRoot都是false

最后一步,code = generate(ast, options),就是根据ast生成render函数和staticRenderFns数组。

最后生成的render如下:

render = function () {
	with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])}
}

src/core/instance/render.js中,我们曾经添加过如下多个函数,这里和render内返回值调用一一对应。

Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = _toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots

这里的staticRenderFns目前是一个空数组,其实它是用来保存template中,静态内容的render,比如我们把例子中的模板改为:

<div id="app">
	<p>这是<span>静态内容</span></p>
	<p>{{message}}</p>
</div>

staticRenderFns就会变为:

staticRenderFns = function () {
	with(this){return _c('p',[_v("这是"),_c('span',[_v("静态内容")])])}
}

从上面的内容,我们可以知道其实template最终还是转换为render函数,这也是官方文档中所说的render函数更加底层。

前面保存了mount = Vue.prototype.$mount,最后又调用了mount方法,我们来看看它干了什么。

打开src/entries/web-runtime.js

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent

这里仅仅是返回了mountComponent的执行结果,跟着代码的步伐,我们又回到了src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ...
  callHook(vm, 'beforeMount')

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

  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false

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

上面的代码我简单的做了一些精简。可以看到首先调用了beforeMount钩子函数,新建了一个Watcher对象,绑定在vm._watcher上,之后就是判断如果vm.$vnode == null,则设置vm._isMounted = true并调用mounted钩子函数,最后返回vm对象。

感觉上似乎有头没尾似得。这里就又不得不提Watcher了,先简单概述一下。

打开src/core/observer/watcher.js

	constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    ...
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''

    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        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
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    if (this.user) {
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      }
    } else {
      value = this.getter.call(vm, vm)
    }

    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    return value
  }

在构造函数中,我们会把expOrFn也就是updateComponent赋值给this.getter,并且在获取this.value的值时会调用this.get(),这里的this.lazy默认值是false,在computed属性中创建的Watcher会传入true

this.get()中,我们会调用this.getter,所以上面的例子中,updateComponent方法会被调用,所以接下来沿着updateComponent再一路找下去。

vm._render

updateComponent中调用了vm._render()函数,该方法在src/core/instance/render.js中。

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const {
      render,
      staticRenderFns,
      _parentVnode
    } = vm.$options
 
 	...
    if (staticRenderFns && !vm._staticTrees) {
      vm._staticTrees = []
    }

    vm.$vnode = _parentVnode
    // render self
    let vnode
      
    vnode = render.call(vm._renderProxy, vm.$createElement)
   	...

    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
  }

在该方法中,其实主要就是调用了vm.$options.render方法,我们再拿出render方法,看看它都干了什么。

render = function () {
	with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])}
}

函数调用过程中的this,是vm._renderProxy,是一个Proxy代理对象或vm本身。我们暂且把它当做vm本身。

_c(a, b, c, d) => createElement(vm, a, b, c, d, false)。我们简单说一下createElement干了什么。a是要创建的标签名,这里是div。接着bdata,也就是模板解析时,添加到div上的属性等。c是子元素数组,所以这里又调用了_c来创建一个p标签。

_vcreateTextVNode,也就是创建一个文本结点。_s_toString,也就是把message转换为字符串,在这里,因为有with(this),所以message传入的就是我们data中定义的第一个vue实例

所以,从上面可以看出,render函数返回的是一个VNode对象,也就是我们的虚拟dom对象。它的返回值,将作为vm._update的第一个参数。我们接着看该函数,返回src/core/instance/lifecycle.js

vm._update

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // 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
    }
  }

mountComponent中我们知道创建Watcher对象先于vm._isMounted = true。所以这里的vm._isMounted还是false,不会调用beforeUpdate钩子函数。

下面会调用vm.__patch__,在这一步之前,页面的dom还没有真正渲染。该方法包括真实dom的创建、虚拟dom的diff修改、dom的销毁等,具体细节且等之后满满分析。

至此,一个Vue对象的创建到显示到页面上的流程基本介绍完了。有问题欢迎吐槽~

挂载在vm上的属性


vm.constructor = {
	
}
// 每个vue示例的id
vm._uid = 0
// 标示是否是vue的实例
vm._isVue = true
// 自身
vm._self = vm
// 调试时的名称
vm._name = 'name'
//如果有原生的Proxy对象,则返回代理对象,否则是vm本身
vm._renderProxy = vm || Proxy

/**
 * 以下是和生命周期相关的属性
**/
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false

// 父结点
vm.$parent = parent
// 根结点
vm.$root = parent ? parent.$root : vm
// 子节点
vm.$children = []
vm.$refs = {}

/**
 * 以下是和事件相关的属性
**/
vm._events = Object.create(null)
vm._hasHookEvent = false

/**
 * 以下是和render相关的属性
**/
vm.$vnode = null
vm._vnode = null 
vm._staticTrees = null
vm.$slots = {}
vm.$scopedSlots = {}
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

/**
 * 以下是和数据相关相关的属性
**/
vm._watchers = []
vm._props = {}
vm._data = {}
vm._computedWatchers


vm.$options = {

}
vm.$mount = function(){

}

目录结构分析

vue源码根目录下有很多文件夹,下面先列出我知道的几个,后续会补充。
Vue
    |-- build 打包相关的配置文件,其中最重要的是config.js。主要是根据不同的入口,打包为不同的文件。
    |-- dist 打包之后文件所在位置
    |-- examples 部分示例
    |-- flow 因为Vue使用了Flow来进行静态类型检查,这里定义了声明了一些静态类型
    |-- packages vue还可以分别生成其它的npm包
    |-- src 主要源码所在位置
        |-- compiler 模板辨析解析的相关文件
        |-- core 核心代码
            |-- components 全局的组件,这里只有keep-alive
            |-- global-api 全局方法,也就是添加在Vue对象上的方法,比如Vue.use,Vue.extend,,Vue.mixin等
            |-- instance 实例相关内容,包括实例方法,生命周期,事件等
            |-- observer 双向数据绑定相关文件
            |-- util 工具方法
            |-- vdom 虚拟dom相关
        |-- entries 入口文件,也就是build文件夹下config.js中配置的入口文件。看源码可以从这里看起
        |-- platforms 平台相关的内容,分为web和weex
        |-- server 服务端渲染相关
        |-- sfc 暂时未知
        |-- shared 共享的工具方法
    |-- test 测试用例

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.