共计 5172 个字符,预计需要花费 13 分钟才能阅读完成。
数据初始化
Vue 实例在建立的时候会运行一系列的初始化操作,而在这些初始化操作里面,和数据绑定关联最大的是 initState。
首先,来看一下他的代码:
function initState(vm) {
vm._watchers = [];
var opts = vm.$options;
if(opts.props) {
initProps(vm, opts.props); // 初始化 props
}
if(opts.methods) {
initMethods(vm, opts.methods); // 初始化 methods
}
if(opts.data) {
initData(vm); // 初始化 data
} else {
observe(vm._data = {}, true /* asRootData */ );
}
if(opts.computed) {
initComputed(vm, opts.computed); // 初始化 computed
}
if(opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch); // 初始化 watch
}
}
在这么多的数据的初始化中,props、methods 和 data 是比较简单的(所以我就不详细介绍了☺),而 computed 和 watch 则相对较难,逻辑较复杂,所以我下面主要讲下 computed 和 watch(以下代码部分为简化后的)。
initState 里面主要是对 vue 实例中的 props, methods, data, computed 和 watch 数据进行初始化。
在初始化 props 的时候 (initProps),会遍历 props 中的每个属性,然后进行类型验证,数据监测等(提供为 props 属性赋值就抛出警告的钩子函数)。
在初始化 methods 的时候 (initMethods),主要是监测 methods 中的方法名是否合法。
在初始化 data 的时候 (initData),会运行 observe 函数深度遍历数据中的每一个属性,进行数据劫持。
在初始化 computed 的时候 (initComputed),会监测数据是否已经存在 data 或 props 上,如果存在则抛出警告,否则调用 defineComputed 函数,监听数据,为组件中的属性绑定 getter 及 setter。如果 computed 中属性的值是一个函数,则默认为属性的 getter 函数。此外属性的值还可以是一个对象,他只有三个有效字段 set、get 和 cache,分别表示属性的 setter、getter 和是否启用缓存,其中 get 是必须的,cache 默认为 true。
function initComputed(vm, computed) {
var watchers = vm._computedWatchers = Object.create(null);
for(var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === ‘function’ ? userDef : userDef.get;
// 创建一个计算属性 watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
if(!(key in vm)) {
// 如果定义的计算属性不在组件实例上,对属性进行数据劫持
//defineComputed 很重要,下面我们再说
defineComputed(vm, key, userDef);
} else {
// 如果定义的计算属性在 data 和 props 有,抛出警告
}
}
}
在初始化 watch 的时候 (initWatch),会调用 vm.$watch 函数为 watch 中的属性绑定 setter 回调(如果组件中没有该属性则不能成功监听,属性必须存在于 props、data 或 computed 中)。如果 watch 中属性的值是一个函数,则默认为属性的 setter 回调函数,如果属性的值是一个数组,则遍历数组中的内容,分别为属性绑定回调,此外属性的值还可以是一个对象,此时,对象中的 handler 字段代表 setter 回调函数,immediate 代表是否立即先去执行里面的 handler 方法,deep 代表是否深度监听。
vm.$watch 函数会直接使用 Watcher 构建观察者对象。watch 中属性的值作为 watcher.cb 存在,在观察者 update 的时候,在 watcher.run 函数中执行。想了解这一过程可以看我上一篇的 vue 响应式系统 –observe、watcher、dep 中关于 Watcher 的介绍。
function initWatch(vm, watch) {
// 遍历 watch,为每一个属性创建侦听器
for(var key in watch) {
var handler = watch[key];
// 如果属性值是一个数组,则遍历数组,为属性创建多个侦听器
//createWatcher 函数中封装了 vm.$watch,会在 vm.$watch 中创建侦听器
if(Array.isArray(handler)) {
for(var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
// 为属性创建侦听器
createWatcher(vm, key, handler);
}
}
}
function createWatcher(vm, expOrFn, handler, options) {
// 如果属性值是一个对象,则取对象的 handler 属性作为回调
if(isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
// 如果属性值是一个字符串,则从组件实例上寻找
if(typeof handler === ‘string’) {
handler = vm[handler];
}
// 为属性创建侦听器
return vm.$watch(expOrFn, handler, options)
}
computed
computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值
下面将围绕这一句话来做解释。
上面代码中提到过,当计算属性中的数据存在与 data 和 props 中时,会被警告,也就是这种做法是错误的。所以一般的,我们都会直接在计算属性中声明数据。还是那个代码片段中,如果定义的计算属性不在组件实例上,会运行 defineComputed 函数对数据进行数据劫持。下面我们来看下 defineComputed 函数中做了什么。
function defineComputed(target, key, userDef) {
// 是不是服务端渲染
var shouldCache = !isServerRendering();
// 如果我们把计算属性的值写成一个函数,这时函数默认为计算属性的 get
if(typeof userDef === ‘function’) {
sharedPropertyDefinition.get = shouldCache ?
// 如果不是服务端渲染,则默认使用缓存,设置 get 为 createComputedGetter 创建的缓存函数
createComputedGetter(key) :
// 否则不使用缓存,直接设置 get 为 userDef 这个我们定义的函数
userDef;
// 设置 set 为空函数
sharedPropertyDefinition.set = noop;
} else {
// 如果我们把计算属性的值写成一个对象,对象中可能包含 set、get 和 cache 三个字段
sharedPropertyDefinition.get = userDef.get ?
shouldCache && userDef.cache !== false ?
// 如果我们传入了 get 字段,且不是服务端渲染,且 cache 不为 false,设置 get 为 createComputedGetter 创建的缓存函数
createComputedGetter(key) :
// 如果我们传入了 get 字段,但是是服务端渲染或者 cache 设为了 false,设置 get 为 userDef 这个我们定义的函数
userDef.get :
// 如果没有传入 get 字段,设置 get 为空函数
noop;
// 设置 set 为我们传入的传入 set 字段或空函数
sharedPropertyDefinition.set = userDef.set ?
userDef.set :
noop;
}
// 虽然这里可以 get、set 都可以设置为空函数
// 但是在项目中,get 为空函数对数据取值会报错,set 为空函数对数据赋值会报错
// 而 computed 主要作用就是计算取值的,所以 get 字段是必须的
// 数据劫持
Object.defineProperty(target, key, sharedPropertyDefinition);
}
在上一篇的 vue 响应式系统 –observe、watcher、dep 中,我有关于 Watcher 的介绍中提到,计算属性 watcher 实例化的时候,会把 options.lazy 设置为 true,这里是计算属性惰性求值,且可缓存的关键,当然前提是 cache 不为 false。
cache 不为 false,会调用 createComputedGetter 函数创建计算属性的 getter 函数 computedGetter,
先来看一段代码
function createComputedGetter(key) {
return function computedGetter() {
var watcher = this._computedWatchers && this._computedWatchers[key];
if(watcher) {
if(watcher.dirty) {
//watcher.evaluate 中更新 watcher 的值,并把 watcher.dirty 设置为 false
// 这样等下次依赖更新的时候才会把 watcher.dirty 设置为 true,然后进行取值的时候才会再次运行这个函数
watcher.evaluate();
}
// 依赖追踪
if(Dep.target) {
watcher.depend();
}
// 返回 watcher 的值
return watcher.value
}
}
}
// 对于计算属性,当取值计算属性时,发现计算属性的 watcher 的 dirty 是 true
// 说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get();
this.dirty = false;
};
// 当一个依赖改变的时候,通知它 update
Watcher.prototype.update = function update() {
// 三种 watcher,只有计算属性 watcher 的 lazy 设置了 true,表示启用惰性求值
if(this.lazy) {
this.dirty = true;
} else if(this.sync) {
// 标记为同步计算的直接运行 run,三大类型暂无,所以基本会走下面的 queueWatcher
this.run();
} else {
// 将 watcher 推入观察者队列中,下一个 tick 时调用。
// 也就是数据变化不是立即就去更新的,而是异步批量去更新的
queueWatcher(this);
}
};
当 options.lazy 设置为 true 之后(仅计算属性 watcher 的 options.lazy 设置为 true),每次依赖更新,都不会主动触发 run 函数,而是把 watcher.dirty 设置为 true。这样,当对计算属性进行取值时,就会运行 computedGetter 函数,computedGetter 函数中有一个关于 watcher.dirty 的判断,当 watcher.dirty 为 true 时会运行 watcher.evaluate 进行值的更新,并把 watcher.dirty 设置为 false,这样就完成了惰性求值的过程。后面只要依赖不更新,就不会运行 update,就不会把 watcher.dirty 为 true,那么再次取值的时候就不会运行 watcher.evaluate 进行值的更新,从而达到了缓存的效果。
综上,我们了解到 cache 不为 false 的时候,计算属性都是惰性求值且具有缓存性的,而 cache 默认是 true,我们也大多使用这个默认值,所以我们说 computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值。