相信对于大多数用过vue的人都知道computed,watch,methods这三者是干嘛的,但是其各自使用场景和底层实现自己真的说的清楚吗? 先看官方的例子:
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})
如果用methods应该怎么实现?
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
如果用watch应该怎么实现?
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
}
}
从代码层面上讲computed和methods看起来没什么区别,要看区别还得从源码实现上理解。而watch的使用就有些区别,它的属性名称不是你要计算后的属性名称,而是其依赖的属性,有点发布订阅的意思。
那具体底层代码如何实现的,区别在哪,我们逐行分析下。
Computed的实现
function initState(vm) {
vm._watchers = [];
var 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,data,computed,watch。具体我们再看initComputed()是如何实现的。
initComputed()
var computedWatcherOptions = {
lazy: true
};
function initComputed(vm, computed) {
//创建一个空对象赋值给watchers
var watchers = vm._computedWatchers = Object.create(null);
//是否服务器端渲染
var isSSR = isServerRendering();
//遍历computed属性
for (var key in computed) {
//获取属性值
var userDef = computed[key];
//判断属性值是函数还是对象,如果是对象会取对象的get属性
var getter = typeof userDef === 'function' ? userDef : userDef.get;
//getter方法为null抛出异常
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
//如果非服务器端渲染
if (!isSSR) {
// 为每个属性创建内部watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
//如果computed中的属性不在组件中,则定义该属性为响应式,如果已存在,比如在data中定义了,则会抛出异常
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else {
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);
}
}
}
}
defineComputed()
如果看过源码的肯定对defineReactive方法有印象,这里定义了一个defineComputed方法:
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
function defineComputed(
target,
key,
userDef
) {
//非服务器端渲染才有缓存
var shouldCache = !isServerRendering();
//userDef就是initComputed方法中定义的每个属性值,如果是方法就执行方法,如果是对象就执行对象的get方法。
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 (sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
("Computed property \"" + key + "\" was assigned to but it has no setter."),
this
);
};
}
//定义成响应式数据,sharedPropertyDefinition对象的lazy属性为true
Object.defineProperty(target, key, sharedPropertyDefinition);
}
里面的核心方法是createComputedGetter()和createGetterInvoker()。
createComputedGetter()
function createComputedGetter(key) {
//返回值是一个函数
return function computedGetter() {
//上面提到的为computed对象属性创建的watcher
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
//computedWatcherOptions默认lazy为true,dirty为true
if (watcher.dirty) {
watcher.evaluate();//调用watcher的get方法,将dirty设置为false
}
//添加依赖收集,只有当值发生变化的时候才会触发通知
if (Dep.target) {
watcher.depend();
}
//注意这里返回的是当前watcher实例的值,这也是为什么computed属性方法需要return
return watcher.value
}
}
}
我们看下这里的watcher.evaluate()做了什么,其实很简单,就是调用当前的get()方法,获取最新值:
watcher.evaluate()
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get();
this.dirty = false;
};
createGetterInvoker()
createGetterInvoker就是调用computed属性对象设置的get方法。
function createGetterInvoker(fn) {
return function computedGetter() {
return fn.call(this, this)
}
}
比如:
computed:{
name:{
get(){
retutn this.firstName+this.sencondName;
}
}
}
computed小结:
通过分析computed源码,我们发现:
1、computed属性也是响应式的;
2、computed属性值可以是函数也可以是对象;
3、computed计算属性是通过watcher来实现的;
4、只有当computed watcher属性dirty为true,响应式依赖发生变化时才会重新计算,这也是为什么computed是计算属性缓存。
5、computed属性最后是一个返回值;
Watch的实现
上面已经提到了watch的入口也是initState()方法中:
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
initWatch()
function initWatch(vm, watch) {
//变量watch对象属性
for (var key in watch) {
var handler = watch[key];
//这里说明watch属性值也可以是数组
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
createWatcher
function createWatcher(vm, expOrFn, handler, options) {
//如果是纯对象,拿到最终的handler回调函数
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
//如果是字符串,则handler是当前组件的某一个方法
if (typeof handler === "string") {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options);
}
高潮来了,看下vm.$watch()的实现:
Vue.prototype.$watch = function (expOrFn, cb, options) {
var vm = this;
//如果是对象,继续遍历执行上一步方法
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
options.user = true;
//创建watcher
var watcher = new Watcher(vm, expOrFn, cb, options);
//如果immediate属性为true,则立即执行回调函数
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, 'callback for immediate watcher "' + watcher.expression + '"');
}
}
//返回一个函数,移除当前watcher,避免内存泄漏
return function unwatchFn() {
watcher.teardown();
};
};
所以watch的核心是vue.$watch方法,其本质也是watcher,当监听属性值发生变化时调用回调函数,由此可见,computed属性函数执行结果是计算依赖值,而watch执行的是一个回调方法,watch实际上比computed更加强大,你可以在回调函数中做很多其他事情。
methods的实现
最后我们再看下methods的实现,同样methods也是在initState中调用的:
if (opts.methods) {
initMethods(vm, opts.methods);
}
initMethods()
function initMethods(vm, methods) {
var props = vm.$options.props;
//遍历methods属性
for (var key in methods) {
{
//methods的属性必须是一个函数,否则会抛出异常
if (typeof methods[key] !== "function") {
warn('Method "' + key + '" has type "' + typeof methods[key] + '" in the component definition. ' + "Did you reference the function correctly?", vm);
}
//methods属性不能在props中有定义
if (props && hasOwn(props, key)) {
warn('Method "' + key + '" has already been defined as a prop.', vm);
}
//methods属性也不能在data属性中定义
if (key in vm && isReserved(key)) {
warn('Method "' + key + '" conflicts with an existing Vue instance method. ' + "Avoid defining component methods that start with _ or $.");
}
}
//最后把methods每个属性方法赋值给vm属性,并重新指向vm
vm[key] = typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
}
}
由此可见methods实际上就是一个普通对象,每个对象属性就是一个方法,每次调用都会执行其对应的方法。