vueのwatchとcomputedはなぜデータの変化と違いを傍受できるのか
29840 ワード
まずフローチャートをください.レベルが限られています.
まず、Vueアプリケーションを作成するとき:
Vueコンストラクション関数ソース:
initStateメソッドでは、data、watch、computedが初期化され、observe関数が呼び出されてdata(Object.defineProperty):
1、observe
observeはinitStateで呼び出され、vueインスタンスのdata属性値にgetter、setter関数が作成され、setterではdep.dependがdepインスタンスのsubs属性にwatcherインスタンスを追加し、getterではdep.notifyが呼び出され、watcherのupdateメソッドが呼び出されます.
2、Dep
Watcherのupdateメソッドはnew Depのnotifyメソッドで呼び出される
3、watch
watchを初期化すると、関数でcreateWatcherが呼び出され、createWatcherで$watchが呼び出され、$watchでnew Watcherインスタンスが呼び出されます.
2、computed
computedを初期化し、new Watcher()を呼び出し、defineComputed関数を使用して計算プロパティをvueインスタンスにマウントし、計算プロパティをテンプレートで使用できるようにします.
まず、Vueアプリケーションを作成するとき:
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
Vueコンストラクション関数ソース:
// Vue
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
//_init , data,watch,computed
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid$3++;
......
// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);
......
};
initStateメソッドでは、data、watch、computedが初期化され、observe関数が呼び出されてdata(Object.defineProperty):
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);//initData observe
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
1、observe
observeはinitStateで呼び出され、vueインスタンスのdata属性値にgetter、setter関数が作成され、setterではdep.dependがdepインスタンスのsubs属性にwatcherインスタンスを追加し、getterではdep.notifyが呼び出され、watcherのupdateメソッドが呼び出されます.
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
* initState
*/
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } re * Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } }; /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } }; /** * Define a reactive property on an Object. */ function defineReactive$$1 ( obj, key, val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val;
//Dep.target Watcher
// dep.addSub, Watcher Dep Watcher if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify();// , Dep notify , Watcher
} }); }
2、Dep
Watcherのupdateメソッドはnew Depのnotifyメソッドで呼び出される
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
// Watcher
// Dep.target, Watcher
// this.get
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this); } }; // subs update Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); if (!config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };
3、watch
watchを初期化すると、関数でcreateWatcherが呼び出され、createWatcherで$watchが呼び出され、$watchでnew Watcherインスタンスが呼び出されます.
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
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
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
}
2、computed
computedを初期化し、new Watcher()を呼び出し、defineComputed関数を使用して計算プロパティをvueインスタンスにマウントし、計算プロパティをテンプレートで使用できるようにします.
var computedWatcherOptions = { lazy: true }
function initComputed (vm, computed) { // $flow-disable-line var watchers = vm._computedWatchers = Object.create(null); // computed properties are just getters during SSR var isSSR = isServerRendering(); for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get;
//getter computed if (getter == null) { warn( ("Getter is missing for computed property \"" + key + "\"."), vm ); } if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ); } // // 。 // 。 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); } } } }function defineComputed (
target,
key,
userDef
) {
var shouldCache = !isServerRendering();//true
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
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
//computed getter , computed
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {//true
watcher.evaluate();// watcher.get , computed
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
以上のコードからwatchとcomputedはnew Watcherインスタンスによってデータの傍受を実現していることがわかりますが、computedのoptionsではlazyがtrueであり、このパラメータは2つの異なるルートを歩んでいます.
computed:テンプレートがデータを取得すると、getter関数がトリガーされ、watcherが最終的に呼び出されます.get、すなわち、対応するコールバック関数を呼び出す.
watch:テンプレートがデータを取得する際にgetter関数をトリガーし、watcherを対応するDep.subsに追加し、その後setterが呼び出されると、Dep.notifyはすべてのwatcherにupdateを行うことを通知し、最終的にwatcherを呼び出す.cb、すなわち、対応するコールバック関数を呼び出す.
3、Watcher
コンストラクション関数はwatchの場合、最後にthisが呼び出されます.getは、プロパティのgetter関数をトリガーし、このWatcherをDepのsubsに追加し、データの変動を通知するときに呼び出す.
Watcherインスタンスのupdateメソッドを呼び出すとrunメソッドがトリガーされ、runメソッドではトリガー関数が呼び出されます.そのdependメソッドはnew Depのdependメソッドを呼び出し、depのdependはWatcherのaddDepメソッドを呼び出し、最終的にこのwatcherインスタンスをDepのsubsプロパティに追加します./** * , , * 。 * $watch()api 。 */ var Watcher = function Watcher ( vm, expOrFn, cb, options, isRenderWatcher ) { this.vm = vm; ...... this.cb = cb;// this.id = ++uid$2; // uid for batching this.active = true; this.dirty = this.lazy; // for lazy watchers ...... this.value = this.lazy ? undefined ? this.get();//computed undefined, watch Watcher.get }; /** * Scheduler job interface. * Will be called by the scheduler. * */ Watcher.prototype.run = function run () { if (this.active) { var value = this.get(); if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value var oldValue = this.value; this.value = value; if (this.user) { try { this.cb.call(this.vm, value, oldValue); } catch (e) { handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\"")); } } else { this.cb.call(this.vm, value, oldValue); } } } }; /** * Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function get () { pushTarget(this); var value; var vm = this.vm; try { value = this.getter.call(vm, vm); } catch (e) { if (this.user) { handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\"")); } else { throw 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 }; /** * Subscriber interface. * Will be called when a dependency changes. * Watcher run */ Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this);// run } }; /** * Depend on all deps collected by this watcher. new Dep depend ,dep depend Watcher addDep */ Watcher.prototype.depend = function depend () { var i = this.deps.length; while (i--) { this.deps[i].depend(); } }; /** * Add a dependency to this directive. */ Watcher.prototype.addDep = function addDep (dep) { var id = dep.id; if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } };