背景
有这样一个 demo,父组件 computed.date 依赖 data.dateRange,并且 data.dateRange 在 created 中被改变了,computed.date 传递给子组件,那么首次传递给子组件的值是基于 this.dateRange 改变后还是改变前计算得出的?
<html>
<body>
<div id="app">
<div>I am Parent Component : name is {{ name }}</div>
<div>------------------------------------------</div>
<comp :date="date" />
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const comp = {
template: `<div>I am Child Component : name is Child</div>`,
props: {
date: {
type: Array,
},
},
data() {
return {
name: 'Child',
}
},
beforeCreate() {
debugger
console.log('子beforeCreate')
},
created() {
debugger
console.log('子created')
},
mounted() {
debugger
console.log('子mounted')
}
}
const vm = new Vue({
el: '#app',
components: {
comp
},
data: {
name: 'Parent',
dateRange: {},
},
computed: {
date() {
// console.log('date...')
const { startTime, endTime, shortcut } = this.dateRange
return [
startTime,
endTime,
shortcut,
]
},
},
beforeCreate() {
debugger
console.log('父beforeCreate')
},
created() {
debugger
console.log('父created')
this.dateRange = { startTime: 1, endTime: 2, shortcut: 3 } // 同步修改
// setTimeout(() => { // 异步修改
// this.dateRange = { startTime: 1, endTime: 2, shortcut: 3 }
// })
},
mounted() {
debugger
console.log('父mounted')
}
})
</script>
</body>
</html>
源码分析
initComputed 相关源码的简写如下:
// 初始化 computed
function initComputed (vm, computed) {
vm._computedWatchers = Object.create(null); // 创建一个全空对象,用于存储 computed watcher
for (var key in computed) {
var userDef = computed[key]; // 此例的 computed.date 是一个函数
var getter = typeof userDef === 'function' ? userDef : userDef.get;
vm._computedWatchers[key] = new Watcher( // 为每一个计算属性新建一个 Watcher
vm,
getter || noop,
noop, // 空函数:function() {}
computedWatcherOptions // { lazy: true }
);
defineComputed(vm, key, userDef); // 将 date 属性添加到 vm 上
}
}
var sharedPropertyDefinition = {
configurable: true,
enumerable: true,
get: noop
set: noop
}
function defineComputed (target, key, userDef) { // 省略了 cache 的判断逻辑
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache // shouldCache: true
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key) : createGetterInvoker(userDef.get);
sharedPropertyDefinition.set = userDef.set || noop;
}
if (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
function createComputedGetter (key) {
return function computedGetter () { // 备注1,后面有提到
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) { // computedWatcher 的 dirty 为 true,表示还未计算过
watcher.evaluate(); // 这个方法会调用 watcher.get,从而调用计算属性的方法,返回的值保存在 watcher.value 中
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
主要做了两件事:
- 为计算属性新建 Watcher 实例,又称
computedWatcher
-new Watcher()
。 - 将计算属性代理到 vm 上 -
defineComputed
。
具体步骤如下:
- 创建一个全空对象
vm._computedWatchers = Object.create(null)
,用于存储computedWatcher
。 - 遍历
computed
中的每一个属性,它的值可能是函数或者对象:- 将函数或对象的 get 函数赋值给 getter。
- 新建 Watcher 实例,其
lazy
属性为true
(注:只有computed watcher
其lazy
属性才为true
)。 - 根据值是函数还是对象对属性描述对象
sharedPropertyDefinition
的get/set
赋值,然后将计算属性代理到 vm 上。
整体流程
Vue.prototype._init = function(options) {
var vm = this;
vm.$options = options;
...
callHook(vm, 'beforeCreate');
initState(vm);
callHook(vm, 'created');
...
if (vm.$options.el) {
vm.$mount(vm.$options.el); // 调用 vm.$mount 开始渲染
}
}
created 调用之后,调用 vm.$mount
开始渲染,先生成渲染函数(render
),然后调用 beforeMount
钩子,接着定义一个 updateComponent
函数,然后为其新建一个 Watcher(又称 renderWatcher),内部调用 this.get(),执行 updateComponent() -> vm._render() -> render()
,由于 render
内部使用了 date 变量,所以会调用 date 的 get 函数,即上面的备注1,然后调用
date() {
// console.log('date...')
const { startTime, endTime, shortcut } = this.dateRange
return [
startTime,
endTime,
shortcut,
]
},
针对本次 demo,此时 this.dateRange 已经是改变后的值,所以首次传递给子组件的值是基于 this.dateRange 改变后计算得出的。如果在 created 中对 this.dateRange 的改变是异步的,那么执行上面这个函数的时候,this.dateRange 还未改变,所以首次传递给子组件的值是基于 this.dateRange 还未改变计算出来的。
Vue.prototype.$mount = function(el) {
vm.$options.render = function anonymous() {
with(this) {
return _c('div', {attrs:{"id": "app"}}, [
_c('div', [_v("I am Parent Component : name is "+_s(name))]),
_v(" "),
_c('div', [_v("------------------------------------------")]),
_v(" "),
_c('comp', {attrs:{"date": date}})
], 1)
}
} // 生成 render 函数
callHook(vm, 'beforeMount'); // 调用 beforeMount 钩子
updateComponent = function () { // 定义 updateComponent 函数
vm._update(vm._render(), hydrating); // 此时 hydrating: false
};
new Watcher(vm, updateComponent, noop, { // 新建 Watcher
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */)
callHook(vm, 'mounted');
}
Vue.prototype._render = function () {
...
var render = vm.$options.render;
vnode = render.call(vm._renderProxy, vm.$createElement);
}
// Watcher 定义
function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {
if (isRenderWatcher) {
vm._watcher = this;
}
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.dirty = this.lazy // for lazy watchers
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
}
this.value = this.lazy ? undefined : this.get();
}
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (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
};
template 与 render 函数的对应
// template
<div id="app">
<div>I am Parent Component : name is {{ name }}</div>
<div>------------------------------------------</div>
<comp :date="date" />
</div>
// render
(function anonymous() {
with(this) {
return _c('div', {attrs:{"id": "app"}}, [
_c('div', [_v("I am Parent Component : name is "+_s(name))]),
_v(" "),
_c('div', [_v("------------------------------------------")]),
_v(" "),
_c('comp', {attrs:{"date": date}})
], 1)
}
})
debugger
debugger 的时候,当断点停到父组件的 created,此时在 chrome Scope 可以看到一开始 date 的值是 ...,鼠标浮动上去,会出现 invoke property getter 的提示,点击 ... 会触发 date 的 getter 函数。