前言
上一篇讲述了Vue源码系列第一章Vue源码系列——— 流程,现在讲下发布订阅模式的实现,本来应该分开讲述data、watch、computed,不过掘金上很多人都讲的很好,也就不再赘述,所以我打算从发布订阅(初始化时收集依赖,改变数据触发发布流程)入手,将props、data、watch、computed顺带一起讲清楚。
依照惯例,先从一段简单的代码入手
// main.js
import App from './App'
new Vue({
el: '#app',
components: { App},
template: `<app :name="name"></app>`,
data () {
return {
name: 'Wayag'
}
}
})
// App.js
import HelloWorld from './components/HelloWorld'
export default {
name: 'App',
props: {
name: {
type: String,
default: ''
}
},
computed: {
total () {
return this.num + 1
}
},
watch: {
total () {
console.log(this.total)
},
num () {
console.log(this.num)
}
},
data () {
return {
num: 0
}
},
methods: {
add () {
this.num++
}
}
}
从vm._render执行_c('app',{attrs:{name: 'Wayag' }})开始分析,这个时候会先进入createComponent函数生成组件的构造函数Sub,并传入data = { attrs: { name: 'Wayag' } }
function createComponent (
Ctor,
data,
context,
children,
tag
) {
var baseCtor = context.$options._base; // Vue
// Vue.extend = function (extendOptions) {
// extendOptions = extendOptions || {};
// var Super = this;
// var SuperId = Super.cid;
// var Sub = function VueComponent (options) {
// this._init(options);
// };
// Sub.prototype = Object.create(Super.prototype);
// Sub.prototype.constructor = Sub;
// Sub.cid = cid++;
// Sub.options = mergeOptions(
// Super.options,
// extendOptions
// );
// if (Sub.options.props) {
// 设置代理,访问vm.name代理访问vm._props_.name
// // Object.defineProperty(Sub.prototype, 'name', {
// // get: function() {
// // return this['_props'][key]}
// // }
// // set: function(val) {
// // this['_props'][key]} = val
// // }
// // })
// initProps$1(Sub);
// }
// if (Sub.options.computed) {
// 设置代理,将computed对象属性添加到Sub.prototype上,设置成响应式数据
// // Object.defineProperty(Sub.prototype, 'total', createComputedGetter)
// initComputed$1(Sub);
// }
// Sub.extend = Super.extend;
// Sub.mixin = Super.mixin;
// Sub.use = Super.use;
// if (name) {
// Sub.options.components[name] = Sub; //
// }
// Sub.superOptions = Super.options;
// Sub.extendOptions = extendOptions;
// Sub.sealedOptions = extend({}, Sub.options);
// cachedCtors[SuperId] = Sub;
// return Sub
// };
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor); // 相当于调用了Vue.extend函数
}
data = data || {};
// function extractPropsFromVNodeData (
// data,
// Ctor,
// tag
// ) {
// var propOptions = Ctor.options.props; // 子组件内部定义的props
// var res = {};
// var attrs = data.attrs; // 父组件传递过来的参数
// var props = data.props;
// if (isDef(attrs) || isDef(props)) {
// for (var key in propOptions) {
// 这里2个checkprop函数就是看是否父组件传递了,并且子组件的props里定义了
// checkProp(res, props, key, altKey, true) ||
// checkProp(res, attrs, key, altKey, false);
// }
// }
// return res
// }
var propsData = extractPropsFromVNodeData(data, Ctor, tag); // { name: 'Wayag' }
// 给data添加hook属性hook: { init: function() {}, prepatch: function() {}, insert:
// function() {}, destroy: function() {}}
installComponentHooks(data);
var name = Ctor.options.name || tag;
// propsData = { name: 'Wayag' },这时propsData、Ctor属性就被保存到了,
// vnode.componentOptions内,Ctor就是构造函数Sub,这里的data: {attrs: {}},attrs里面的属性
// 被移除了,propsData的赋值和data.attrs移除都是在extractPropsFromVNodeData函数内实现的。
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
return vnode
}
这里有必要解释一下initComputed$1函数里面的createComputedGetter,Object.defineProperty(target, key, {get: createComputedGetter, set: noop}),computed属性不允许直接修改。
function createComputedGetter (key) {
return function computedGetter () {
// 这里this._computedWatchers[key]有值,是在vm._init初始化initComputed之后才有的,
// 所以其实在这里将computed的属性变为响应式时并不会执行computedGetter函数,这是在
// user Watcher中监听了或者在render函数渲染时访问vm.total,也就是computed属性时才会调用这
// 里的computedGetter函数,进而
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
// 是否进行重新计算,重新计算都会涉及依赖收集,主要看computed watcher依赖了哪个属性。
watcher.evaluate();
}
// 执行完watcher.evaluate函数,computedWatcher已经从栈里面被删除了,Dep.target也从computedWatcher到userWatcher或者渲染Watcher了。
if (Dep.target) {
// 这里就是“月老牵线”的重点,让data属性xxx与userWatcher或者渲染watcher建立关系
watcher.depend();
}
return watcher.value
}
}
}
现在知道子组件vm是如何访问到父组件传递的参数的了,如下代码所示: vm.$options.propsData
function initInternalComponent (vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
var parentVnode = options._parentVnode;
opts.parent = options.parent;
opts._parentVnode = parentVnode;
var vnodeComponentOptions = parentVnode.componentOptions;
// vm.$options.propsData = parentVnode.componentOptions.propsData
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children;
opts._componentTag = vnodeComponentOptions.tag;
}
前面讲了vm.$options获取父组件传递的参数,接下来进入文章主题,发布订阅模式的前提,就是将数据变成响应式数据。在_init函数里面。
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);
}
}
// 初始化props
function initProps (vm, propsOptions) {
var propsData = vm.$options.propsData || {};
var props = vm._props = {};
var loop = function ( key ) {
keys.push(key);
var value = validateProp(key, propsOptions, propsData, vm);
defineReactive$$1(props, key, value);
if (!(key in vm)) {
proxy(vm, "_props", key);
}
};
for (var key in propsOptions) loop( key );
toggleObserving(true);
}
// 初始化data
function initData (vm) {
var data = vm.$options.data;
data = vm._data
var keys = Object.keys(data);
while (i--) {
var key = keys[i];
proxy(vm, "_data", key);
}
observe(data, true /* asRootData */);
}
// initData时的observe函数
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
// 如果存在.__ob__属性说明此data已经被响应式处理过,之后进来就不再需要重新被定义为响应式
// 场景:如果name是来自父级传入的对象,data中两个数据引用了同一个name对象,此时就会进入下面第
// 一个if语句。
// props: {
// name: {
// type: Object,
// default: () => {
// return {}
// }
// }
// },
// data () {
// return {
// num: 0,
// b: this.name,
// c: this.name
// }
},
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++;
}
return ob
}
// initData时observe函数里面的Observer函数
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
// 将对象添加__ob__属性,数据是否已经被监听
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);
}
};
// 将数据变成响应式函数,包括data,和props上的数据
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
// props和data里的属性用于收集依赖的框dep
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);
// 其实这里监听的是vm._data[key]
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 除非手写getter、setter函数,不然都是直接使用val
var value = getter ? getter.call(obj) : val;
// new Watcher()执行时会访问props或者data内定义属性的将自身这个watcher赋值给Dep.target =
// this,所以dep收集依赖是看当前是执行哪个new Watcher(),基本是三个:computedWatcher、
// userWatcher、renderWatcher
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
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
// 初始化computed
function initComputed (vm, computed) {
// 先定义vm._computedWatchers
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();
// debugger
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
if (!isSSR) {
// 往vm._computedWatchers里面加入computedWatcher,将computed里面的属性加入到
// vm._computedWatchers里面,后续访问
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else if (process.env.NODE_ENV !== 'production') {
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);
}
}
}
}
// 初始化watch
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();
}
};
// Watcher函数
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
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$2; // 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 {
// parsePath函数,watch里面的变量会调用这个函数,然后访问到vm.total,然后会触发
// initComputed里面做的响应式监听,触发evaluate函数,执行computedWatcher的get函数。
// function parsePath (path) {
// if (bailRE.test(path)) {
// return
// }
// var segments = path.split('.');
// return function (obj) {
// for (var i = 0; i < segments.length; i++) {
// if (!obj) { return }
// obj = obj[segments[i]];
// }
// return obj
// }
// }
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
);
}
}
// 如果这里是computedWatcher的话,this.lazy = true,是不会执行this.get的,是当触发下面的
// evaluate函数的时候执行的(userWatcher或者renderWatcher访问computed)
// Watcher.prototype.evaluate = function evaluate () {
// this.value = this.get();
// this.dirty = false;
// };
this.value = this.lazy
? undefined
: this.get();
};
// this.get
Watcher.prototype.get = function get () {
// 这里就是当前watcher去调用get的时候会将当前watcher赋值给Dep.target
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
};
说明:watch监听了total属性,或者渲染函数读取到了vm.total都会触发computed的total,触发computed的过程被称为“月老牵线”,假如是watch里面监听了total属性,那么会触发computedWatcher的evaluate 函数,也就是Watcher.prototype.get,然后会执行computed里面的total函数,如果(一般来说)依赖了initdata里面的xxx属性的话,会触发vm.xxx,xxx开始使用dep收集依赖,此时Dep.target = computedWatcher,就将computedWatcher(total)收集到了xxx属性里面的dep.subs里,反过来computedWatcher也将dep收集到了this.deps里,之后将computedWatcher从栈里面删除,Dep.targer从computedWatcher到userWatcher或者renderWatcher,执行watcher.depend(),让Computedwatcher里面this.deps收集的dep依次执行dep.depend收集dep.target,此时Dep.target是userWatcher或者renderWatcher,就把他们收集到了xxx.subs里面。
Watcher.prototype.depend = function depend () {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};
讲完了订阅(收集依赖的过程),后面讲发布(数据更新)的过程。 前面我们知道了data属性的dep收集到了subs也就是订阅者们,通过修改data属性数据,会触发上面的Object.defineProperty中的set函数,其实set函数只需要做两件事:1.将val = newVal赋值 2.执行dep.notify(),实现发布
Object.defineProperty(vm._data, key, {
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
//
dep.notify();
}
})
接下来看触发过程
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
Watcher.prototype.update = function update () {
if (this.lazy) { // this.lazy是当watcher是computedWatcher的时候,只需要将this.dirty =
// false就行了,后续访问到computed属性的时候就可以触发重新计算。
this.dirty = true;
} else if (this.sync) {
this.run();
} else { // 其他的userWatcher或者renderWatcher会进入这里的条件
queueWatcher(this);
}
};
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true; // 保存watcher.id,避免重复添加watcher
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
if (!waiting) {
waiting = true;
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue();
return
}
// nextTick是一个微任务,作用是将flushSchedulerQueue放到下一个微任务(或宏任务)再执行,这
// 样做的好处就是同步多次修改同一个数据时,肯定会多次触发该数据收集的watcher,前面已经将重
// 复触发watcher的步骤过滤掉了,加入nextTick不是一个微任务(也可能是宏任务)而是立即执行的
// 话,只能获取第一次修改的值,而微任务则会因为异步获取到最后修改的值。
nextTick(flushSchedulerQueue);
}
}
}
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
// timerFunc是一个异步,优先级Promise > setImmediate > setTimeout
var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
// flushSchedulerQueue函数
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
// 这里为什么不用let len = queue.length,因为其实这里的queue是不断变化的,根据什么变化?是根
// 据变化的子组件决定的,那我们知道flushSchedulerQueue是在微任务里面,而在当前组件中,数据变
// 化会执行依赖该数据的watcher,而且只有当前组件才有机会进入这里,那根据props传值的子组件如何
// 能够根据父组件传递的数据将其watcher加入进来这里的queue队列中呢?其实是下面的watcher.run()
// 做到的,而且是当前组件的渲染watcher。如下图可以看出执行流程(从下往上),渲染watcher去执行
// 渲染过程也就是patch过程的时候,prepatch之后执行updateChildComponent中有一句
// props[key] = validateProp(key, propOptions, propsData, vm)这句会触发props的set过程,
// 之后便是notify -> update -> queueWatcher往queue队列里面去添加watcher,所以前面为什么说
// queue.length是一直变化的了。这个过程也可以说是父组件data属性变化时,子组件如何根据父组件
// data的变化而重新渲染。
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
// 其实就是生命周期updateBefore
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null; // 释放has[id]
// 主要逻辑,执行this.get(),并且执行回调this.cb()
watcher.run();
}
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
// 重置
resetSchedulerState(); // has = {}, queue.length = 0
}
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;
// 调用ComputedWatcher和userWatcher的回调函数并传入value,这样就可以在回调函数中获
// 到值。
this.cb.call(this.vm, value, oldValue);
}
}
};
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
};