上篇博文已经讲到了执行完import Vue from “vue”,Vue构造函数和实例已经拥有了很多的属性和方法,那么我们接下来继续进行。
这个是引入App.vue
作为模版导入。
请记住_Ctor
这个对象。
执行src/router/index.js
,代码如下
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "Home",
component: Home
},
{
path: "/about",
name: "About",
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/About.vue")
}
];
const router = new VueRouter({
routes
});
export default router;
引入VueRouter
依赖,使用Vue.use()
方法注册使用VueRouter
插件。传入配置的路由数组生成一个路由实例。将路由实例对象对外暴露。
执行src/store/index.js
,代码如下
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {}
});
引入Vuex
,使用Vue.use()
方法注册使用Vuex
插件。同样对外暴露出实例。
还记得之前的Vue
构造函数嘛,其自身拥有的config
属性下的一个属性,大概意思就是,开发环境下,Vue 会提供很多警告来帮你对付常见的错误与陷阱。而在生产环境下,这些警告语句却没有用,反而会增加应用的体积。此外,有些警告检查还有一些小的运行时开销,这在生产环境模式下是可以避免的。
终于开始初始化Vue构造函数了,我们找到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)
}
代码很简单,执行原型上面的_init
方法,并且把我们的参数传递进去。
紧接着找到定义_init
的代码
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${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) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
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)
console.log('--beforeCreate--')
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
console.log('--created--')
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
console.log('开始$mount')
vm.$mount(vm.$options.el)
}
}
}
我们顺序执行代码,首先是合并配置。
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
这块的作用就是如果是组件初始化,那么就走
initInternalComponent(vm, options)
否则走
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
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
}
让人一眼觉得很难的就是一串if判断了,就是这块。
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
这块的作用是如果当前组件不是抽象组件并且存在父级,那么就通过while循环来向上循环,如果当前组件的父级是抽象组件并且也存在父级,那就继续向上查找当前组件父级的父级,直到找到第一个不是抽象类型的父级时,将其赋值vm.$parent
,同时把该实例自身添加进找到的父级的$children
属性中。这样就确保了在子组件的$parent
属性上能访问到父组件实例,在父组件的$children
属性上也能访问子组件的实例,这样就自上而下将父子组件串联了起来。
紧接着往实例身上初始化一些属性。
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)
}
}
实例上新增了_events
属性并将其赋值为空对象,用来存储事件。接着,获取当前组件父组件注册的事件赋给listeners
,如果listeners
不为空,最后通过调用updateComponentListeners
函数,将父组件向子组件注册的事件注册到子组件实例中的_events
对象里。
父组件既可以给子组件上绑定自定义事件,也可以绑定浏览器原生事件。这两种事件有着不同的处理时机,浏览器原生事件是由父组件处理,而自定义事件是在子组件初始化的时候由父组件传给子组件,再由子组件注册到实例的事件系统中。
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 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)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
console.log('--render--');
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
这块代码最主要的就是给实例上挂载_c
和$createElement
这两个方法,这两个方法是干嘛的呢?还记得入口文件的render
函数嘛,我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要render
方法,无论我们是用单文件.vue
方式开发组件,还是写了 el
或者 template
属性,最终都会转换成 render
方法。
而这里的两个方法,第一种它是被模板编译成的render
函数使用,而vm.$createElement
是用户手写render
方法使用的, 这俩个方法支持的参数相同,并且内部都调用了createElement
方法。
最后将$attrs
、$listeners
设置成响应对象。
执行完上述的初始过程之后就开始了beforeCreate
钩子。
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
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, result[key])
}
})
toggleObserving(true)
}
}
这段代码主要是用来初始化inject
的,细心的同学可能会产生疑问,provide
和inject
不应该是一起的嘛。在Vue事件系统中,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。那么为什么要先初始化inject
呢?设想一下这个情况
使用一个注入的值作为数据入口:
const Child = {
inject: ['foo'],
data () {
return {
bar: this.foo
}
}
}
使用一个注入的值作为一个 property 的默认值:
const Child = {
inject: ['foo'],
props: {
bar: {
default () {
return this.foo
}
}
}
}
初始化的data
和props
可能会使用到inject
,那么理所应当,初始化inject
要在初始化initState
前。而数据初始化完毕后,再由provide
进行数据注入。
provide
和 inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
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 && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initState
函数则进行了数据的一系列初始化工作,比如初始化props
、methods
、data
、computed
、watch
等,并对数据进行劫持,绑定getter
和setter
,并通过proxy
将初始化的property
进行代理,让你能通过this
关键字进行访问,所以这也就是为什么props
、data
等属性不能重名的原因。
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
上面也已经解释了为什么初始化provide
会在初始化inject
和初始化数据之后
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
如果是函数的话,更改其执行的内部this
指向,让你在函数内部也能使用this
,如果是对象的话,直接是对象传递给实例_provided
。
执行完上述的初始化之后,开始执行created
钩子函数。这也解释了为什么能在created
钩子函数里能获取props
、methods
、data
等的原因。
执行完上述的代码之后我们来到了这一步,这也是在_init(options)
里面。
if (vm.$options.el) {
console.log('开始$mount')
vm.$mount(vm.$options.el)
}
这个就是开始挂载了,如果你在options
里面写了el
属性,那么就是Vue帮你挂载,又或者是你手动挂载,最后的结果都是调用$mount
方法。
开始调用$mount
,我们来到src/platforms/web/entry-runtime-with-compiler.js
这个文件。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to or - 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')
}
console.log('编译render')
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
进行缓存,接着进行了重写。我们大致看下,执行了什么逻辑。
首先,它对 el
做了限制,Vue 不能挂载在 body
、html
这样的根节点上。接下来的是很关键的逻辑 —— 如果没有定义 render
方法,则会把 el
或者 template
字符串转换成 render
方法。在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render
方法,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render
方法,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render
方法,重要的实情说三遍。无论我们是用单文件.vue
方式开发组件,还是写了 el
或者 template
属性,最终都会转换成 render
方法,而这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions
方法实现的。也就是说在挂载dom
之前,render
函数已经生成完毕。
而恰巧,我们初次初始化Vue的时候,传入的就有render
这个属性。所以我们继续往下进行。调用mount
方法。hydrating
这个属性是服务器渲染相关,我们这里不必理会它。
继续执行下去它最终会调用位于src/core/instance/lifecycle.js
的mountComponent
函数。
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
}
代码最核心的部分就是这
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
初始化一个渲染watcher
,要了解一点的是Vue主要分四种watcher
,负责视图更新的渲染watcher
,负责计算属性的computed watcher
,以及用户自定义的watch watcher
,还有一种就是负责响应式数据更新的普通watcher
,而初始化渲染就属于第一种。
我们来到定义Watcher
类的地方,src/core/observer/watcher.js
文件。
/* @flow */
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
noop
} from '../util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '../util/index'
let uid = 0
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* 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
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
console.log('watcher--lazy')
this.dirty = true
} else if (this.sync) {
console.log('watcher--sync')
this.run()
} else {
console.log('watcher--queueWatcher')
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
console.log('watcher--run', this.cb)
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)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
大致看下执行逻辑,注意这个属性_watcher
。由于v2版本的观测建立在了组件的颗粒度上,所以每个组件的实例都会有这个属性,这里保存着所有视图更新的Dep
实例。
if (isRenderWatcher) {
vm._watcher = this
}
还记得Vue提供的一个方法$forceUpdate
嘛,强制视图更新,其内部实现就是遍历_watcher
,来达到视图的再次渲染。
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
非常让你容易混淆的是这个
vm._watchers.push(this)
这个_watchers
存放着组件实例的所有watcher
,包括渲染watcher
、user watcher
、computed watcher
以及普通watcher
。这个属性的作用就是为了移除watcher
用的。
我们继续往下执行,最后会执行
this.get()
我们看下get
函数的逻辑
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
}
首先我们先看这两个函数pushTarget
和popTarget
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
设置了一个唯一全局变量Dep.target
,以及存放watcher
的栈数组。这个也就保证了在同一时间只有一个watcher
被计算。为什么要压栈和出栈呢?设想一个这么个情况,当在父组件进行watcher
收集时,这个时候执行到了子组件内部,这个时候就应该开始子组件的依赖收集工作,所以更改Dep.target
的指向,使其指向子组件的watcher
,在收集完成之后,进行出栈操作,这样也就能保证回退到父组件也能继续进行依赖收集。
紧接着执行
value = this.getter.call(vm, vm);
我们看下这个getter
函数,发现其竟然就是updateComponent
,为什么呢?我们看这里
new Watcher(vm, updateComponent, noop, { // 这里被当作参数传入
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// watcher
constructor (
vm: Component,
expOrFn: string | Function, // 这里进行接收
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
)
// 这里又进行了计算
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
)
}
}
所以上面的getter
函数就是updateComponent
函数。也就是执行这句
vm._update(vm._render(), hydrating)
执行了这个vm._render()
,这个方法最终最调用_createElement
方法生成虚拟dom,这个时候就会对数据进行访问,触发每个响应数据的getter
,我们康康defineReactive
定义响应式数据的地方。
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
console.log('getter', value)
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
存在全局唯一的Dep.target
,就会执行dep.depend()
,我们再来看看定义Dep
类的地方
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
这个时候,执行这句Dep.target.addDep(this)
,就相当于执行watcher
类里面的这句,为什么呢?因为这个时候Dep.target
就是当前的实例watcher
,所以就相当于实例watcher
对象,调用了自身的方法addDep
,把当前的this
也就是Dep
类的实例对象传进去,最后执行dep.addSub(this)
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
而这句话也就是执行了Dep
类里面的这个函数
addSub (sub: Watcher) {
this.subs.push(sub)
}
把当前的watcher
实例放进了subs
数组里。随着组件内的响应式数据的触发,这样也就完成了依赖收集工作,每个响应式数据都维护自身的Dep
类实例,而Dep
类实例里面的subs
数组放着对这个响应式数据的订阅。而执行这句traverse(value)
就是为了进行子属性递归收集,最后收集完成之后进行依赖清空
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
至于为什么会被清空,就是为了在用户使用不到的时候进行了派发更新,什么是使用不到呢?比如你在组件内使用v-if
指令隐藏了一部分dom,而这部分dom恰巧有响应式数据,如果不去清除,随着组件进行重新渲染,毫无疑问是一种性能浪费,这属于Vue的一种性能优化。
执行完渲染虚拟dom之后,开始执行vm._update()
进行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__
进行diff
,也就是常说的diff
算法进行比对。由于我们是初始化渲染,所以oldVnode
为空,所以它会走这行代码
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
然后执行patch
函数时,也就是位于src/core/vdom/patch.js
文件的这块
return 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 ' +
', or missing
. 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
}
由于我们是初次渲染,所以里面的diff
比较,我们通通跳过,直接到这里
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
createElm
的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中,当渲染到子组件时,则通过createComponent
函数来创建子组件,所以渲染的时候是先子后父的,这也解释了为什么是子组件mounted
之后才是父组件的mounted
。最后渲染完毕,插入到body上。
至此,我们的Vue页面已经全部渲染完毕了。
你可能感兴趣的:(vue,vue,源码)