首先我们先自己尝试实现一下数据监测。所谓数据监测就是当一个值改变时,用到这个值得地方做出相应改变。里面的核心就是 Object.defineProperty
。
Object.defineProperty
var obj = {a: 1};
Object.defineProperty(obj, a, {
enumerable: true,
configurable: true,
get: function(){
// 当调用get方法是,就表明用到了该属性。
//这边的问题就卡在 需要收集什么
}
})
收集的依赖是什么
我们收集依赖的目的就是当值改变时,依赖的部分也要做出相应的改变。
但是现在的问题是依赖的地方会有很多,类型也不一样。可能是template里面用到,可能是computed里面计算用到。也有可能用户自己watch监听。先抛开用到地方应该怎么变,想想,一个动作触发一个事件,不就是回调函数的逻辑吗?
拿最简单的watch来说:
var callback = function (newVal, oldVal) {
// do something
}
vm.$watch(obj.a, callback)
这么一看就简单了,我们收集的依赖就是”回调函数”。
就像上面说的,一个属性用到的地方可能会有很多,因此需要收集的回调函数也很多,因此我们用一个数组来保存。
同时呢,这是个通用方法,我们可以封装一下提出来。
因此上面的代码可以改成
var obj = {a: 1};
function defineReactive (data, key, val) {
var dep = [];
Object.defineProperty(obj, a, {
enumerable: true,
configurable: true,
get: function(){
dep.push(callback); // 先不管callback哪来的
},
set: function(newVal){
if(val === newVal) return
val = newVal;
dep.forEach(function(callback, index){
callback();
})
}
})
}
去耦合
可以把dep封装成一个对象
class Dep {
constructor(id){
this.id = id;
this.deps = [];
}
addSub( sub ){
this.subs.push(sub)
}
removeSub (sub){
reomve(this.subs, sub)
}
notify(){
this.deps.forEach(function(callback, index){
callback();
})
}
}
function defineReactive (data, key, val) {
var dep = new Dep();
Object.defineProperty(obj, a, {
enumerable: true,
configurable: true,
get: function(){
dep.addSub(callback); // 先不管callback哪来的
},
set: function(newVal){
if(val === newVal) return
val = newVal;
dep.notify();
}
})
}
Watcher
上面虽然借助callback来帮助理解,但真正实现肯定不可能真是callback。任何一个函数在不同的上下文中执行结果都不相同,光拿到要执行的函数肯定不行,还得有执行的上下文。因此我们可用个类包装一下,observe中不关心怎么执行callback,只需要通知一个监听者自己去做更新操作就好。这个监听者就是watcher.
class Watcher {
constructors(component, getter, cb){
this.cb = cb // 对应的回调函数,callback
this.getter = getter;
this.component = component; //这就是执行上下文
}
//收集依赖
get(){
Dep.target = this;
this.getter.call(this.component)
Dep.target = null;
}
update(){
this.cb()
}
}
既然我们将callback换成了Watcher实例,注意这边,Dep里面收集的Watcher实例,可不是Wacther构造函数。那么在数据的getter方法中就要想办法拿到。我们将实例存放在Dep中,一个函数对象上。Dep.target = this
,当依赖收集完就销毁 Dep.target = null
。因此Observe
代码可以改成。
function defineReactive (data, key, val) {
var dep = new Dep();
Object.defineProperty(obj, a, {
// ....
get: function(){
if(Dep.target){
dep.addSub(Dep.target); // Dep.target是Watcher的实例
}
},
// ...
})
}
class Dep {
//...
notify(){
this.deps.forEach(function(watcher, index){
watcher.update();
})
}
}
VUE源码
大概流程通了,我们再做点完善。Observe,Dep, Watcher三个关系弄清楚了。现在的问题是,怎么收集依赖和回调。举例来说:
<div id="app">
<input type="text" v-model="name"/>
<div>{{name}}</div>
</div>
new Vue({
el: "#app",
data: {
name: "默认值",
age: 29
}
})
一: template中的依赖
name
属性直接在template中用到。那么只要触发render,就可以收集到依赖。当然,收集到依赖后,需要及时更新。把DOM中的{{name}}
替换成Data
中对应的值。
这部分代码在 lifecycle.js
的mountComponent
方法中,可以精简为
export function mountComponent(){
...
callHook(vm, 'beforeMount') //生命周期函数
var updateComponent = () => {
// 先通过render收集依赖,再通过update将虚拟DOM中的值同步到真实节点中
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, emptyFunc, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate') //生命周期函数
}
}
}
vm._isMounted = true
callHook(vm, 'mounted') //生命周期函数
}
每个模板实例化一个Watcher实例。这也与官网的流程图一致
$watch
<div id="app">
<input type="text" v-model="name"/>
<div>{{name}}</div>
</div>
new Vue({
el: "#app",
data: {
name: "默认值",
age: 29
},
watch: {
age: function(newValue, oldValue){
console.log("新的值为:" + newValue)
}
}
})
传进来的options会在initState中处理
export function initState (vm: Component) {
//vm
vm._watchers = []
const 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)
}
}
.....
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
可以看到对于options
中的watch
其实就是执行$watch
方法。
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
options = options || {}
options.user = true
// expOrFn: key, cb: callback watch中的每个key都实例出Wacther实例
const watcher = new Watcher(vm, expOrFn, cb, options)
// immediate: 如果为true就立刻执行一次,否则第一次进来不执行,当data改变才会触发执行
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
computed
同watch,计算属性computed也是在initData中处理。
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 可以看到,VUE为每个computed属性也都生成了一个watcher实例。
//而这边的getter就是计算属性的计算函数。必须先计算一次才能触发依赖的属性的get方法,收集依赖
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
结论
这边就不再放出VUE的数据监听部分源码,可以自己阅读watcher.js
,dep.js
,observer/index.js
。总体代码和我们自己实现的很像,只是比我们代码更缜密,多了些其他功能。比如$watcher之后会返回一个取消函数,可以取消监听。
就像上面分析的,一个监听流程的完成必须包含:
- 数据本身可被监听(定义了set,get)。
- 这个数据被收集了依赖。也就是有人监听。
我们知道VUE为template,watch,和computed中的属性实例化了Watcher。而只有在data中的属性才会再initState时进行监听操作。因此我们可以得出结论,
- data中的属性才可以被监听。
- 只有在templete中用到的属性或被手动watch的,或计算属性用到的,数值改变时才会执行相应的操作。