终于自己写了一个超简版的gitHub地址.
Vue中的三种Watcher
1.computed Watcher
在initComputed的时候,在非服务端的渲染的情况下,会循环key in computed对象,为每一个key和其对应的value(函数/对象)创建watcher,
watcher的特点是创建时watcher.lazy=true,watcher.get()不会立马执行,watcher.value初始化时为undefined
2.user Watcher
在initWatch的时候,会循环key in watch对象,通过createWatcher(vm, key, handler)为每一个key和其对应的handler(函数/对象)创建watcher
user watcher的标志是watcher.user-true,user Watcher会在创建的时候直接执行this.get(),得到的值赋值给watcher.value
3. 渲染 Watcher
每个组件只有一个渲染Watcher,vm._watcher就是这个组件的渲染watcher
渲染watcher的创建是在执行mountComponent方法时,创建后会直接执行this.get方法
<!--UpdateComponrnt方法主要是1.vm._render()生成Vnode;2.vm._update将1中生成的虚拟Vnode转换为真实dom,这样可以实现组件的更新和重新渲染-->
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* true说明是渲染watcher*/);
以一个最简单的页面开始
<!--App.vue-->
<template>
<div>
<div>oriValue的值: {{oriValue}}</div>
<div>computed对象里oriValueAfterComputed的值(跟随oriValue变化):{{oriValueAfterComputed}}</div>
<div>watch对象里oriValueWatch的值(跟随oriValue变化):{{oriValueWatch}}</div>
<button @click="oriplus">oriValue加1</button>
</div>
</template>
<script>
export default {
data() {
return {
oriValue:1,
oriValueWatch:""
}
},
methods:{
oriplus(){
this.oriValue++
console.log("this.oriValueAfterComputed",this.oriValueAfterComputed)
}
},
computed:{
oriValueAfterComputed:{
get(){
return this.oriValue+" Computed"
},
set(value){
this.oriValueAfterComputed = "setValue"+" oriValueAfterComputed :"+value
}
}
},
watch:{
oriValue:{
handler(newValue){
this.oriValueWatch = newValue + " Watch"
}
}
}
}
</script>
从这几个简单的属性,我们可以知道
oriValueAfterComputed随着oriValue变化
oriValueWatch随着oriValue变化
也就是说,当oriValue变化,需要通知computed watcher和user watcher 将oriValueAfterComputed,oriValueWatch更新,最后通过渲染watcher将页面更新渲染
Vue中实现数据监听的原理是
以需要监听oriValue为例,在oriValue对应的dep对象的subs数组中放入与oriValue相关的watcher对象,然后在每个相关watcher的deps数组里面放入deps对象。
当oriValue变化时,触发watcher.notify方法,将该watcher的deps数组里面的每个watcher都执行一次。deps数组的最后一个watcher是渲染watcher,前面的watcher实现值的更新获得最新的值,渲染watcher执行更新视图
通过debugger来调试代码
initData
为了讲解清晰,请只关注oriValue这个值
initState(vm)-> initData(vm) -> obserbve(data,true)=>ob = new Observer(value) =>this.walk(value)=>defineReactive?1(obj, keys[i]);
function defineReactive?1 (obj,key,val) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
var getter = property && property.get;
var setter = property && property.set;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
dep.notify();
}
});
}
在defineReactive?1这个方法中,我们设置了oriValue这个值的getter 和 setter方法,在读取oriValue这个值的时候,会触发getter函数,给oriValue赋值的时候,触发setter方法。
这里特别需要说明,defineReactive?1只是创建了属于这个key的dep对象以及定义了key的setter和getter函数,并没有执行这两个函数!!!
在这个在defineReactive方法里面,我们需要注意
var dep = new Dep() //这里创建了dep对象
可以有截图可以看到,在defineReactive?1方法的参数key为oriValue的时候,这个dep对象的id是6,在后面关于oriValue的dep操作,值的都是这个id为6的dep对象
同理,在后面关于oriValueWatch的dep操作,都是这个id为7的dep对象
initComputed
为了代码简单明了,将默认为 非服务端渲染 以及 使用缓存,以及 省略异常情况报警告的代码
var computedWatcherOptions = { lazy: 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;
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
defineComputed(vm, key, userDef);
}
}
上面的代码只要是创了建watchers对象,为computed对象的属性创建对应的watche对象,如下所示(本例子中computed对象只有oriValueAfterComputed这个属性值)
watchers["oriValueAfterComputed"] = new Watcher(vm:vm,
getter: function(){ return this.oriValue+" Computed"},
noop,
computedWatcherOptions:{ lazy: true };
)
接着为computed的key定义getter和setter函数,上面的initState同理,取值是触发getter方法,赋值时触发setter方法。
从下面的defineComputed可以看到,getter方法是createComputedGetter方法执行的结果
function defineComputed (
target,
key,
userDef
) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = userDef.set || noop;
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
而createComputedGetter返回的是一个computedGetter函数,这个computedGetter函数做了什么呢? 首先,computedGetter先拿到key对应的watcher对象,在这个例子中就是
watchers["oriValueAfterComputed"] = new Watcher(vm:vm,
getter: function(){ return this.oriValue+" Computed"},
noop,
computedWatcherOptions:{ lazy: true };
)
然后如果这个watcher的dirty属性为true的话,就重新执行watcher.evaluate(),也就是watcher.getter(), 在本例子中getter方法就是
function(){
return this.oriValue+" Computed"
}
上面这段代码,最后返回value值,也就是上述代码的执行结果,结果会存储在value属性中,dirty一直false的时候,getter函数不会重新执行(也就是缓存的概念),getter函数触发最后获得的结果就是该key在computed对象中定义的get()方法的执行结果
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
//dirty一直false的时候,evaluate函数,也就是getter函数不会重新执行
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
createComputedGetter还有一个地方暂时没有分析,等getter方法被触发的时候我们再分析这里
if (Dep.target) {
watcher.depend();
}
至于setter方法就没有过多的需要分析的了
sharedPropertyDefinition.set = userDef.set || noop;
setter方法就是该key在computed对象中定义的set()方法
和initData一样,defineComputed只是创建了属于computed对象中key的Watcher对象以及定义了key的setter和getter函数,并没有执行这两个函数!!!没有执行!!!所以初始化后这个key对应的watcher对象的value还是undefined
initWatch
在分析initwatch前先说明一下,和computed对象的key创建的watcher不一样,watch对象里的key对应的watcher是创建后会里面先执行一次,watch.value初始化后不为undefined
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 {
<!--handler可以对象,也可以是一个方法,或者是字符串-->
createWatcher(vm, key, handler); //本例子中handler不为array
}
}
}
function createWatcher (
vm,
expOrFn,
handler,
options
) {
<!--如果handler是一个对象,则handler是它的handler属性-->
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
<!--如果handler是一个字符串,handler为vm[handler]-->
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options); //这里就是为每一个key创建一个watcher对象
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
// 省略...
}
}
//省略...
};
}
initWatch方法总结来说就是做了这么几件事情
1.拿到watch对象中每个key对应的handler函数
2.创建对应key的watcher,将handler作为参数创建watcher对象
3.watcher内部执行this.get(),也就是执行handler方法,这里的get()可能会触发initData中的getter函数
new Watcher的过程
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
//省略无关代码
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
//这样getter获得的
this.getter = parsePath(expOrFn);
}
this.value = this.lazy
? undefined
: this.get();
};
这段代码就是决定watcher的get方法是否在初始化后直接执行
computed对象中的属性的对应watcher没有初始化的时候执行,因为在创建的时候回传进了lazy:true
watch对象属性对应的watcher没有传入,默认lazy为undefined,所以会执行this.get()
this.value = this.lazy
? undefined
: this.get();
};
那么get方法执行经历了什么,为什么和实现视图变化有莫大的关系呢?
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
//省略错误处理代码
} finally {
//省略deep属性为true的处理方法
popTarget();
this.cleanupDeps();
}
return value
};
以我们本次的例子为例,下面是这次例子的watcher对象
watch:{
oriValue:{
handler(newValue){
this.oriValueWatch = newValue + " Watch"
},
immediate:true
}
}
1.pushTarget(this);
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
全局创建了一个targetStack数组,用来存放当前运行的watcher
在initWatcher中,var watcher = new Watcher(vm, expOrFn, cb, options);这里创建了一个watcher对象,并且watcher的get函数是立马执行的,所以当前的watcher是oriValue的watcher,将这个watcher压进targetStack这个栈内,并把Dep类的target设为这个Watcher对象
2.执行getter方法,读取oriValue的值,触发oriValue的getter方法,把oriValue对应的dep放进上面Dep.target的subs数组中,也就是当前的watcher,oriValue的Watcher
var vm = this.vm;
value = this.getter.call(vm, vm);
这里的getter函数读取了vm.orivalue的值,所以会触发在initData里面给oriValue定义的getter函数,看看下图
还记得这个id为6的dep对象吗,就是在initData方法里的defineReactive方法中定义的那个dep对象,接下来就是执行dep.depend方法,
dep.depend方法简单来说,就是把dep对象放入目前Dep.target中的watcher对象的deps数组中,再把这个watcher放进dep对象中,他们大概就这这么个关系
这时候oriValue的dep对象里面就有对应的watcher了
3.popTarget(this);
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
最后,把当前的watcher出栈,如果栈为空,则Dep.target为undefined
initWatch到此完成,总结来说,这个方法就是为watch对象里面的key创建对应的watcher,并执行this.get()方法读取一次key的值,触发getter函数,让这个key的dep对象和watcher建立关系
mountComponent方法
前面的初始化完成后,会调用mountComponent,这里面主要更新组件的方法是updateComponent,UpdateComponent方法主要是执行组件的_render函数,生成一个虚拟dom,也叫做vnode,再调用_update方法将vnode转化成真实dom。这个方法在本文中不会过多分析,大家知道是做什么用的就好
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
看看mountComponent方法是怎样创建渲染watcher的
function mountComponent (
vm,
el,
hydrating
) {
//省略无关代码
var updateComponent;
//省略无关代码
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher,这个是是否为渲染watcher的标记 */);
//省略无关代码
}
上面的代码非常明显地,创建了一个渲染watcher,
Watcher = function Watcher (
vm:vm,
expOrFn:updateComponent,
cb:noop,
options,
isRenderWatcher:true
)
上面说了,options这个参数并没有传入{lazy:true},所以this.get()会执行,这里的this.get()
把目前这个渲染watcher压入栈
Dep.target为当前渲染watcher
执行updateComponent方法
渲染wather出栈
UpdateCompoent中有一步骤
vnode = render.call(vm._renderProxy, vm.$createElement);
render就是组件的渲染函数(本例子组件的render函数如下所示)
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", [
_c("div", [_vm._v("oriValue的值: " + _vm._s(_vm.oriValue))]),
_c("div", [
_vm._v(
"computed对象里oriValueAfterComputed的值(跟随oriValue变化):" +
_vm._s(_vm.oriValueAfterComputed)
)
]),
_c("div", [
_vm._v(
"watch对象里oriValueWatch的值(跟随oriValue变化):" +
_vm._s(_vm.oriValueWatch)
)
]),
_c("button", { on: { click: _vm.oriplus } }, [_vm._v("oriValue加1")])
])
}
var staticRenderFns = []
render._withStripped = true
export { render, staticRenderFns }
上面代码每行依次执行 当执行到
_c("div", [_vm._v("oriValue的值: " + _vm._s(_vm.oriValue))]),
会触发oriValue的getter函数,就和initWatch一样,将oriValue的deps对象放进渲染watcher的deps数组内,将渲染watcher放进dep对象的subs数组内,**需要注意的是,这时候render还没执行完,updateComponent也没有执行完,所以Dp.target还是渲染watcher
oriValue的dep对象的subs数组有
[oriValue对应的userWatcher,组件渲染watcher]
当执行到
_vm._s(_vm.oriValueAfterComputed)
会触发oriValueAfterComputed的getter方法,这个getter方法在initComputed的时候被定义了
function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
<!--这里就是获取initComputed中定义的计算watcher-->
var watcher = this._computedWatchers && this._computedWatchers[key];
还记得我们在initComputed里面new Watcher的时候传进去的options是{lazy:true}
所以这个watcher的dirty为true,会执行watcher.evalute()
evaluate方法也不过多分析,主要就是执行这个key在computed对象中定义的get方法,返回执行结果,将dirty设为false
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
这个函数里面也执行了this.get(),这是把这个computed Watcher压入栈,Dep.target就变成了这个computed watcher
this.get方法中this.getter.call(this)这一步会调用执行computed中该key对应的get方法
computed:{
oriValueAfterComputed:{
get(){
return this.oriValue+" Computed"
},
//·····
},
可以看到上面函数的执行会读取oriValue的值
好了,相信都能猜到了,将oriValue的deps对象的deps数组放进computed watcher,将dep对象的subs数组放入computed watcher对象
oriValue的dep对象的subs数组有
[oriValue对应的userWatcher,组件渲染watcher,oriValue对应的computed watcher]
完成后将computed watcher出栈,现在Dep.target就变成了渲染watcher
最后computedGetter返回的是watcher.value,也就是执行结果,所以_vm.oriValueAfterComputed能computed方法执行后的值
computedGetter方法里面,可以看到在evaluate函数执行完后,在返回watch.value前还执行了这么一段代码的时候
if (Dep.target) {
watcher.depend();
}
Watcher.prototype.depend = function depend () {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
这个watch.depend其实就是把当前watcher(本例中是oriValueAfterComputed的computedWatcher)的dep数组里的每一个dep对象放进现在栈顶watcher对象(本例子中是组件渲染watcher)的deps数组中,再在这个deps数组的dep对象的subs数组中加入Dep.target自身 至于为什么有这么一步骤,我猜测是,当oriValue不作为页面元素渲染,只作为computed对象get函数的参数时,oriValued的dep对象里面是没有渲染watcher的,也就是说oriValueAfterComputed对应的watcher对象的deps数组也是没有渲染watcher的,那么当watcher.notify的时候,是不会更新的vnode,也就不会更新dom
当执行到
_vm._s(_vm.oriValueWatch)
这时候读取了_vm.oriValueWatch,在前面initData的时候,给oriValueWatch定义了一个id为7的dep对象,现在Dep.target为渲染watcher,所以getter方法会 把id=7的dep对象放入Dep.target的watcher对象的deps 数组,把Dep.target放入这个dep对象subs数组中
现在oriValueWatch对应的dep对象的subs数组就有了 [ 组件渲染watcher ]
最后updatecomponent函数执行完,渲染watcher从targetStack出栈,Dep.target为undefined
现在我们来总结一下目前有的watcher对象和dep对象,梳理清楚他们的关系对接下来的watcher.notify和dep.update很有帮助
dep.notify()通知视图更新
好了,现在watcher们和dep们都准备好了,我们现在可以看看改变数据会发生什么事情了
这个set函数会触发dep.notify(),例如现在是oriValue = oriValue+1,就会触发oriValue的setter方法
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();//这时候id=6的dep对象的subs数组遍历,每一个watcher依次执行update方法
}
};
Watcher.prototype.update = function update () {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
//不符合上面两者情况,会执行queueWatcher(this)方法,蚕食是当前watcher
queueWatcher(this);
}
};
<!--
把watcher对象push到queue中
重复的id将会被跳过,除非前一个同id的watcher已经执行完
-->
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
//如果queue正在执行,则按照id顺序插入到queue数组相应的位置
//如果这个前一个同id的watcher已经会执行完了,则这个watcher会进入queue等待下一次flushing
// 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);
}
// queue the flush
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
<!--根据watcher的id从小到大来排序,因为渲染watcher是组件里最后创建的watcher,所以id最大,
在queue数组中也是排到最后,最后执行-->
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
}
}
Watcher.prototype.run = function run () {
if (this.active) {
//新的value
var value = this.get();
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
var oldValue = this.value;
this.value = value;
if (this.user) {
//如果是user watcher,需要执行一次this.cb,也就是watcher里面对应key的handler函数
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);
}
}
}
};
所以当oriValue+1的时候,watcher更新流程如下
最后回到上面的问题
针对红框内的猜测,我在代码中vue.runtime.esm.js文件和App.vue中注释掉了这两段代码。
现在页面只剩下computed的值oriValueAfterComputed渲染页面了(去掉oriValueWatch是因为oriValueWatch的dep也有渲染watcher)
结果如下
可以看到,上图中this.oriValueAfterComputed已经发生变化了,但视图并没有更新
把注释取消,视图随着oriValueAfterComputed的值更新了
本文作为个人学习记录,如果其中说得不妥当的地方,请路过的大佬们多多指出,谢谢各位!!!