初始化 methods
初始化method时, 只需要循环选项中的methods对象, 将每个属性挂载到vm上
// 初始化方法
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
// 遍历vm.$options中的methods
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
// method不是 函数 警告
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
// 如果 props 中已经 有 这个 key 警告
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
// 如果 vm[key] 实例上已经什么过 警告
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
// 把method方法 挂载 到 vm实例上
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
这里先声明一个变量props, 用来判断 methods中的方法是否和props发生了重复,然后 for...in循环methods对象。
- 校验方法是否合法
methods中的某个方法已经存在于vm中, 并且方法名以 $ 或 _开头,会发出警告
- 将方法挂载到vm中
判断 methods[key]是否存在,如果不存在,则将 noop 赋值到 vm[key]中, 如果存在,该方法通过 bind 改写它的this后,再赋值给vm[key]中。
这样我们就可以通过vm.x访问 methods中的x方法了。
初始化data
data 中的 数据最终会保存 到vm_data中,然后 在vm上设置一个代理,使得通过 vm.x可以访问到 vm._data中的x属性。 最后调用 observe函数将 data 转换为响应式数据。
// 初始化 data
function initData (vm: Component) {
let data = vm.$options.data
// data 是 函数 ,执行并转换为响应式 , 否则 就是 自己
data = vm._data = typeof data === 'function'
? getData(data, vm) // getData 把 data 里面 的值 转换为响应式 ,并返回 对象类型
: data || {}
// data 如果不是 对象 设置默认值为 空对象
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
// 这里 methods 仅仅警告,下面的proxy 依旧会代理
if (process.env.NODE_ENV !== 'production') {
// 方法 中已经 存在 该 key 警告 (在生产环境下 是可以 重复命名的)
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// props 中 已经存在 会警告, 这里进入 后就不会 调用 proxy代理了
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
// 不是以 $ or _ 开头的话, 把 _data上的key 代理 到vm上。 (不能以 $ _开头设置 key)
// 直接 访问 vm[key] 就可以访问 vm._data[key]
proxy(vm, `_data`, key)
}
}
// observe data
// 观察data, 转换为响应式
observe(data, true /* asRootData */)
}
我们得到data后,保存在 data变量中, 判断data 的类型,如果是函数,执行函数得到返回值 并赋值给data 和 vm._data。
最终得到data 的值应该是 Object类型, 否则开发环境会警告, 并且 设置data 为 一个空对象的默认值。
然后判断 当前循环的key 是否存在与methods中
判断 props中是否存在某个属性与key相同
如果data中某个 key 与methods发生了重复 ,依然会将 data 代理到实例中, 当如果与props重复 , 不会将data 代理到实例中。
最后proxy 函数实现代理功能。
初始化 computed
computed是定义在 vm上的一个特殊的getter方法。 vm上定义getter方法时, get并不是用户提供的函数, 而是vue.js内部的一个代理函数, 在代理函数中 结合 watcher实现缓存 与 收集依赖等功能。
计算属性的结果会被缓存,如何知道 计算属性的返回值时候发生了变化呢? 这是结合 watcher 的 dirty属性来分辨的。 当dirty 为true 时, 说明需要重新计算,否则,不需要重新计算。
当计算属性中的 内容发生变化后, 计算属性 的watcher 和 组件的watcher 都会得到通知。
计算属性的watcher 会把dirty 属性设置为true,此时不会执行计算。
同时 组件的watcher 也会得到通知,从而 执行render函数 进行重新渲染操作,由于要重新执行render,所以会重新读取计算属性的值,此时计算属性的watcher上的dirty为true,会重新就上计算属性的值,用于本次渲染。
计算属性的一个特点是缓存。 计算属性函数所依赖的数据在没有发生变化的情况下,会反复读取计算属性,而计算属性并不会反复执行。
// 设置 watcher 时的 lazy 为true , 实例化时 告诉 watcher 生成一个 供计算属性使用的watcher 实例
const computedWatcherOptions = { lazy: true }
function initComputed(vm: Component, computed: Object) {
// $flow-disable-line
// 保存计算属性的watcher 实例
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
// 计算属性 在SSR 环境,只是一个 普通的 getter 方法
const isSSR = isServerRendering()
// 遍历 computed 对象
for (const key in computed) {
const userDef = computed[key]
// 获取 getter , 函数直接本身,对象获取对象上的get函数
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
// 服务端环境 没必要
if (!isSSR) {
// create internal watcher for the computed property.
// 比如: fullName : function(){return this.firstname + this.lastname}
// 由于 computed 的wathcer lazy,所有开始并不会 执行 get.
// new Watcher(vm, updateComponent, noop, 。。。) 这个 组件更新的watcher ,会 对模板仅仅访问,获取到当前
// computed的 值,才出发 sharedPropertyDefinition.get 函数.
// watcher.evaluate() 会 执行 computed的函数 比如上面的 return this.firstname + this.lastname
// 这样 fullname这个watcher 就会 保存 firstanme 和 lastname 的 dep
// 记录完之后 继续, 这个时候 的Dep.target 时 updateCOmponent对于的 Watcher.
// computed 的 watcher.depend()。 会把 computed里面的 所有dep (firstname, lastname的dep),分别收集
// updateCOmponent对于的 Watcher。 这样 ,当 firstname或者 lastname变化时, 它们的dep 执行 dep.notify
// 通知 每个 watcher 执行 update 方法 。 而 computed第 三个参数 是noop。所有没有回调执行。
// 而 updateCOmponent 的watcher update 会让页面 重新 render
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
我们先声明了变量 computedWatcherOptions, 作用是在实例化watcher时,通过参数告诉 watcher 类应该生成一个 供计算属性使用的watcher实例。
随后vm._computedWatchers属性用来保存所有计算属性的watcher实例。
Object.create(null)创建出来的对象没有原型, 它不存在proto属性
接下来, for...in循环computed 对象, 初始化每个计算属性。
随后判断 当前环境不是服务端渲染时, 创建watcher实例。
第二个参数的getter 其实是用户设置的计算属性 get函数。
最后,判断当前循环到的计算属性的名字是否存在vm中。 如果不存在,使用 defineComputed函数在vm上设置一个计算属性。
defineComputed函数
// 默认属性描述符
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
// 属性的 getter 和 setter 根据 userDef 设置
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
// 非服务端环境下 计算属性才缓存
const shouldCache = !isServerRendering()
// userDef 可能是 函数 也可能是 对象 {get: fn, set: fn}
// 如果是函数 ,将函数作为 getter函数
// 如果是对象, 将get方法作为getter方法,set方法作为setter方法
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache ?
createComputedGetter(key) :
createGetterInvoker(userDef) // 不需要缓存 ,仅仅执行 函数即可
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get ?
shouldCache && userDef.cache !== false ?
createComputedGetter(key) :
createGetterInvoker(userDef.get) :
noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function() {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
先定义了 sharedPropertyDefinition , 提供一个默认的属性描述符。
接着函数 接受三个参数 target, key, userDef. 属性的getter和setter根据 userDef的值来设置。
然后函数中shouldCache 变量 ,在非服务端渲染环境下,计算属性才有缓存。
在定义计算属性时, 判断userDef的类型。 如果是函数, 则将函数理解为getter, 如果是对象, 则将队形的get 方法作为getter. set方法作为setter.
计算属性的缓存与响应式功能 主要在于是否将getter方法设置到createComputedGetter函数执行后的返回结果。
// 缓存 key
function createComputedGetter(key) {
// 返回的getter 在 initComputed的时候不会执行 (lazy 为 true constructor时 不会执行 this.get)
// 等到 lifecycle 中 beforeMount 生命周期 之后
// 对 new Watcher(vm, updateComponent, noop, ...) // (updateComponent 先调用渲染函数 获取一份最新的Vnode节点树, 然后通过 _update方法 对最新的 Vnode和 旧Vnode进行对比,更新DOM节点。)
// new Watcher 会 由于, 没有lazy 会执行 this.get() ,此时的Dep.target 为 updateComponent的watcher(组件的watcher),而 updateComponent 会 对 所有节点进行对比 ,所有 会触发 computed 这里的 getter
// 这里 watcher.dirty(constructor 的时候 this.dirty = this.lazy), evaluate 此时 执行 this.get
// 把 computed 对应的 watcher 保存 到 Dep.target中 , 执行 computed的函数 或 get 获取 返回值,popTarget之后 Dep.target 继续变为 updateComponent的Watcher(组件的watcher)
// 执行 到 Dep.target 时 其实 就是 updateComponent的Watcher(组件的watcher),watcher.depend 把 computed 的 watcher中 用到 的dep依赖列表循环(所有的computed的 dep 依赖列表), 都 添加 当前 updateComponent的Watcher (组件的watcher)
// 这样 当 computed 的 getter 中 有变化,会 触发 updateComponent的Watcher (组件的watcher)的更新,即页面 更新
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// dirty 设置 为 true之后 ,下一次读取 计算属性才会 重新计算
if (watcher.dirty) {
watcher.evaluate()
}
// 把 当前 读取计算属性的watcher 添加到 计算属性所有的依赖列表中
// 这样 依赖列表 有变化 ,都会 通知 当前 这个watcher 更新(比如更新视图)
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
这个函数是一个高阶函数,接受一个参数key并方法另一个函数computedGetter。
先使用key 读取 watcher 并赋值给 变量 watcher。 如果watcher 存在,那么判断的wathcer.dirty是否为true。
但计算属性所依赖的状态发生变化时,会将wathcer.dirty设置为true。 这样当下一次读取计算属性时,会发现watcher.dirty为true, 此时会重新计算返回值, 否则就直接使用之前的计算结果。
随后判断Dep.target是否存在,如果存在,则调用watcher.depend方法。 读取计算属性的那个watcher添加到计算属性说依赖的所有状态的 依赖列表中。
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
....
if (options) {
this.lazy = !!options.lazy
} else {
this.lazy = false
}
this.dirty = this.lazy
this.value = this.lazy ? undefined: this.get()
/**
* 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()
}
}
evaluate方法的逻辑很简单,执行this.get方法重新计算一下值,然后将this.dirty设置为false。
watcher.depend方法会遍历thisdeps属性(保存了计算属性用到的所有状态的dep实例,而每个属性的dep实例也保存了它的所有依赖),并依次执行dep实例的depend方法。
depend方法,将组件的watcher实例添加到dep实例的依赖列表中。
组件的watcher观察到计算属性中用到的状态发生变化时,组件的wathcer 会收到通知,从而进行重新渲染操作。
初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
初始化watch的时候要判断watch选项不是 浏览器的原生watch时,才能初始化watch操作。 因为 Firefox浏览器中的 Object.prototype上有一个watch方法。
由于watch使用时,值 可以是 回调函数、方法名,包含选项的对象。
watch:{
a: function(val, oldVal){},
b: 'someMethod',
c: {
hander: function(val, oldVal){},
deep: true,
immediate: true
},
e: [
function fn1(val, oldVal){},
function fn2(val, oldVal){}
],
g.a: function(val, oldVal){}
}
实现思路: 循环watch选项,将对象中每一项依次调用vm.$watch方法来观察表达式 或 computed 在 vue.js实例上的变化即可。
由于watch 选项的值支持不同类型,所有要做适配
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
先处理数组的情况, 如果handler的类型是数组,那么遍历数组,将每一项依次调用 createWatcher函数来创建watcher. 如果不知直接调用 createWatcher函数创建一个Watcher.
createWatcher函数主要负责处理其他类型的 handler 并调用 vm.$watch 创建 Watcher观察表达式。
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 如果是对象,options设置为handler
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 如果是字符串,从vm中取出方法赋值给handler
if (typeof handler === 'string') {
handler = vm[handler]
}
// 初始化 watch
return vm.$watch(expOrFn, handler, options)
}
handler的类型有三种情况:字符串、函数、对象。
- 如果是函数,不用特殊处理, 直接传递给vm.$watch即可。
- 如果是对象,说明用户设置了包含选项的对象,把options的值设置为handler,并且将变量handler设置为 handler对象的handler方法。
- 如果是字符串, 从 vm中取出方法,将它赋值给handler变量即可。
针对不同类型的值处理完毕后, handler变量是回调函数,options为vm.watch即可完成初始化watch的任务。
初始化provide
provide选项应该是一个对象或者是返回一个对象的函数。 可以使用ES2015Symbl作为key,当它只有在原生支持 Symbol和 Reflect.ownkeys的环境下工作。
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
这里判断provide的类型是否是函数,是函数执行将返回值赋值给vm._provided。否则直接将变量赋值给vm._provided。
总结
Vue.js整体生命周期分为4个阶段: 初始化阶段、模板编译阶段、挂载阶段、卸载阶段。
初始化阶段结束后,会触发created钩子函数。 在created钩子函数与beforeMount钩子函数之间的阶段叫模板编译阶段, 这个阶段在不同的构建版本中不一定存在。
挂载阶段在beforeMount钩子函数与mounted期间,挂载完毕后, Vue.js处于已挂载阶段。 已挂载阶段会持续追踪状态的变化,当数据变化时, watcher会通知虚拟DOM重新渲染视图。
在渲染钳触发beforeUpdate钩子函数,渲染完毕后触发updated钩子函数。
当vm.$destroy被调用时,组件进入卸载阶段。 卸载前会触发 beforeDestroy钩子函数, 卸载后悔触发destroyed钩子函数。
new Vue()被执行后, Vue.js进入初始化阶段,然后选择性进入模板编译与挂载阶段。