「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
在前面的几篇文字 initState(1)-(3) 中,我们详细讨论了 initProps 的过程,现在我们继续讨 initState 中其余的逻辑,在此之前先回顾 initState 整体逻辑:
initProps初始化vm._props,将propsOptions中的数据变为响应式的initMethods初始methodsinitData初始化datainitComputed初始化computedinitWatcher初始化watch
值得一提的时候,在上面这一系列的初始化中,props、methods、data、computed 的优先级是按这个列出顺序由高到底排列的,这是因为这些都要代理到 vm 上(或直接添加到 vm),属性不能有重复。这一点很好立即,大家都是通过 this.xx 访问这四项的。
二、initMethods
2.1 方法位置:
src/core/instance/state.js -> function initMethods
2.2 方法作用:
处理 vm.$options.methods ,即我们创建组件时传递的 methods 对象,此外校验其中的 key 不能和 props 重复,key 对应的值必须为函数类型,最后将 methods 中的方法都复制到 vm 上,这个不是代理,是真实复制;
另外,这些方法的 this 被绑定为 vm,这就保证了你可以肆意在 methods 的方法中使用 this 访问 vm 上的所有。
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (typeof methods[key] !== 'function') {
// 校验类型必须为函数
}
if (props && hasOwn(props, key)) {
// 属性不能和 props 中的 key 重复
}
if ((key in vm) && isReserved(key)) {
// 不能是 vue 保留的方法名
}
}
// 复制到 vm 上
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
三、initData
3.1 方法位置:
src/core/instance/state.js -> function initData
3.2 方法作用:
- 判断
data如果是个工厂函数,调用工厂函数获取data对象 - 判断
data中的属性不能和props或methods中的属性重名 - 调用
proxy()方法将data中的属性都代理到vm上,便于this.xx获取data - 调用
observe()方法将data中的数据变为响应式,核心还是defineReactive
function initData (vm: Component) {
let data = vm.$options.data
// 得到 data 对象
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
// ....
}
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]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
// key 和 methods 重名了
}
}
if (props && hasOwn(props, key)) {
// 和 props 重名了
} else if (!isReserved(key)) {
// 代理属性到 vm 上
proxy(vm, `_data`, key)
}
}
// 调用 observe 为 data 对象上的数据设置响应式,
observe(data, true /* asRootData */)
}
四、initComputed
方法位置:src/core/instance/state.js -> function initComputed
方法作用:遍历 vm.$options.computed 选项,为每一个 key 创建一个 Watcher 实例,默认是 lazy 执行,然后 调用 defineComputed() 方法将每个 key 代理到 vm 上,最后校验 computed 中的 key 不能和 data、props、methods 中的 key 重复;
值得一提的是,computed 本身使用 watcher 实现的,所以大家在使用上也会感受到计算属性 computed 和自定义的监听器很相像,宏观的区别如缓存和异步等这里不再赘述,后面随着源码的深入会仔细探讨这些的。
function initComputed (vm: Component, computed: Object) {
// 注意这个 vm._computedWatchers 对象,后面的 createComputedGetter 方法中要用到了
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
// 遍历 computed 对象
for (const key in computed) {
// 获取 key 对应的值,即 getter 函数
const userDef = computed[key]
// 判断 getter 必须是个函数,这个 getter 就是定义 computed 时的函数
const getter = typeof userDef === 'function' ? userDef : userDef.get
// ....
if (!isSSR) {
// 为每个 computed key 创建 watcher 实例,并且添加到 watchers 中,每个 key 一个
// watchers 即 vm._computedWatchers 对象
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions // 配置项,lazy 为 true
)
}
if (!(key in vm)) {
// 代理 computed 对象中的属性到 vm 上
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// 非生产环境判重,computed 的 key 不能和 data、props 中的 key 相同,警告信息输出已省略
if (key in vm.$data) {
} else if (vm.$options.props && key in vm.$options.props) {
} else if (vm.$options.methods && key in vm.$options.methods) {
}
}
}
}
4.1 Watcher 类
类的位置:src/core/observer/watcher.js
类的作用:创建 Watcher 实例的时候将会解析传入的表达式并收集表达式中的依赖,当表达式的值发生变化时触发传入的回调。
有个概念叫做渲染 watcher,这是一种特殊的 watcher,后面到渲染阶段的时候就会遇到,他是用于渲染组件模板用的 watcher,当模板绑定的数据发生变化时将会重新渲染模板。
前面有提到过,当数据发生变化时,就会触发一个响应式数据的 setter,而这些 setter 早就做好了依赖收集,这些 watcher 依赖这个数据早就被 dep 保存起来了,接着 setter 在更新值之后就会触发 dep.notify(),在 dep.notify() 中就会逐个调用 watcher.update 方法来更新,这个 watcher.update 就是 Watcher.prototype.update 方法。
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()
}
// .... 原型方法暂时省略去
}
4.2 Watcher 构造函数
4.2.1 Watcher 构造函数接收参数:
vm:Vue实例expOrFn:表达式或者函数,创建computed时接收到的是computed定义函数或一个空函数cb:回调,初始化computed时接收到的是个空函数options:watcher的配置,初始化computed时接收到的是{ lazy: true }isRenderWatcher:是否为渲染 watcher,初始computed时没传入,所以接收到的undefined,即 false
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions // 配置项,lazy 为 true
)
4.2.2 构造函数的工作
这段描述中出现在
Watcher构造函数中的this均为Watcher的实例
- 如果
isRenderWatcher为true,则将当前渲染watcher赋值给vm._watcher属性 - 根据接收到
options参数初始化this.deep/this.user/this.lazy/this.sync/this.before属性,初始化computed创建的watcher传入的options是{ lazy: true },所以此时this.lazy = true,其余的都是false; - 将
cb参数赋值给this.cb,另外this.dirty赋值为this.lazy,这个是个有用的属性,这个东西是computed计算属性有缓存而watch监听器没有缓存的重点 - 如果
expOrFn是个函数类型,例如初始化computed时,expOrFn传的就是函数,将this.getter赋值为expOrFn;如果不是函数就要调用parsePath(expOrFn)解析成函数; - 给
this.value赋值,如果this.lazy为true,就赋值undefined,否则调用this.get()方法;
4.3 defineComputed
将 computed 的每个 key 代理到 vm,这个方法也涉及了不少细节,下个主题再具体展开
4.4 判重处理
如果非生产环境下还要校验 computed 的 key 不能和 props、methods、data 的 key 重复复,如果重复则输出警告信息;
五、总结
本篇小作文着重介绍了 initState 在 initPorps 之后的 initMethods、initData、和initComputed。
其中 initMethods 就是将 methods 中的方法复制到 vm 上,并且绑定方法的 this 为 vm;
initData 方法调用 observe 方法将 data 中的数据及其子代嵌套的数据都变为响应式;
initComputed 方法是初始化 computed 中的计算属性,所谓计算属性本质是一种 Watcher 实例,只不过是 lazy 的 Watcher,这个 lazy 体现在不直接求值,而是等调用 Watcher.prototye.evaluate 方法时才求值。
另外这里出场了 Vue 中的重磅角色 Watcher 类,他致力于解析收到的 expOrFn(表达式或函数),收集其中的依赖,expOrFn 发生变化时调用其传入的 cb 执行更新逻辑。这个在后面讲到渲染 watcher 时就会明确很多,渲染 watcher 的 cb 时 updateComponent 方法,这个方法会重新计算虚拟 DOM,这个过程中就会重新获取 vm 上的数据,此时得到的就是新的数据,进而得到新的虚拟 DOM;
下一篇将会详述 initComputed 未尽的细节方法,与之并行的就是 Watcher` 的原型方法讨论。