写在前面:本文为个人在日常工作和学习中的一些总结,便于后来查漏补缺,非权威性资料,请带着自己的思考^-^。
前文链接:了解一下Vue - [Vue是怎么实现响应式的(一)]
前言:上一篇文章简单介绍了基于data的Vue响应式的实现,这篇将进行一点扩展,data变更会自动触发computed进行重新计算,从而反映到视图层面,那这个过程又是怎么做到的呢?
从Vue实例生成过程的initComputed说起
顾名思义,在initComputed中主要进行的工作是对computed进行初始化,上代码:
function initComputed (vm, computed) {
const watchers = vm._computedWatchers = Object.create(null) // 用于存放computed 相关的watcher
for (const key in computed) { // 遍历computed,为每一个computed属性创建watcher实例,这个watcher实例的作用后面会体现
// 这里可以看出,平时我们的computed一般都是函数形式的,但很多时候我们也可以写成{get() {}, set() {}},这种对象形式
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop, // 此函数参数会在watcher实例的get方法中进行执行
noop,
computedWatcherOptions // {dirty: true},可以翻一下之前的class Watcher代码或者找源码看一下,这个options其中的一个作用就在于控制实例化watcher的时候是否先执行一次get() 方法,这个get方法内部会对参数传进来的getter进行执行
)
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
// function defineComputed
function defineComputed ( // 整体来说此函数的作用就是通过defineProperty定义getter/setter将computed中的属性代理到vm上
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering() // 非服务端渲染,则为true
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key) // computed的计算结果会被缓存,不需要每次访问computed属性时都重新进行计算
: userDef // computed 不使用缓存的情况
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
实例化Vue期间,对computed的处理,做了:
- 将computed[key]的值作为getter参数,实例化一个Watcher对象(至于Watcher对象的作用,后面会提到);
- 将computed[key]的值(其实是经过包装的)作为getter,通过defineProperty将computed[key]代理到vm[key];
说到$mount的执行过程
$mount包含了模板编译、render函数生成... 不再赘述
和响应式相关的是在$mount中实例化了一个render Watcher,前文已经有过标注,在实例化Watcher中会执行get()函数,从而执行render函数,在render函数的执行过程中势必会读取页面渲染使用到的computed属性,触发其getter:
// computed属性的getter
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key] // watcher就是initComputed时传入computed[key]作为getter参数实例化的watcher
if (watcher) {
if (watcher.dirty) { // 如果是首次读取computed[key],一般watcher.dirty为true
watcher.evaluate()
/**
evaluate () {
this.value = this.get()
this.dirty = false // 执行完get之后就会将dirty置为false,这样下次读取computed[key]时就不会再重新计算
}
执行watcher.get(),在这个函数内部会做几个事情:
执行pushTarget函数,将当前的Dep.target置为当前watcher实例
执行computed[key],计算得到结果赋值给watcher.value
如果computed[key]函数内容是通过几个data计算得到值,则将会触发data的getter,
这将会把这个几个data的dep对象作为依赖添加至watcher的deps列表,同时将当前watcher添加至这些dep的subs列表中,
通俗一点说,这个过程对于当前watcher来说就是依赖收集过程,将其依赖的项添加至deps中
对于某一个data的dep来说,就是将当前watcher添加至其观察者列表subs中
执行完以上过程,就会将Dep.target重置为之前的值,
*/
}
if (Dep.target) {
watcher.depend() // 这一步也很重要,此处是将当前watcher的依赖也加入到Dep.target的依赖列表中
/**
为什么要有这一步呢?
因为当前的Dep.target在执行完watcher.evaluate之后就被重置回了上一个Dep.target,一般来说当前的Dep.target
就是render Watcher
设想有这种情况:某一个data的属性x并没有直接用于render,那么在render执行过程的依赖收集x就不会被添加
到render Watcher的deps中,x的dep.subs中也没有render Watcher 也就是说之后如果对x进行重新赋值,则不会
通知render Watcher,此时还没有问题,但时如果x被computed用到,由于computed没有setter,则x被重新赋值
通知到computed Watcher去重新计算,但是computed并没有直接通知render Watcher的方法,这个时候render就不会
重新执行,页面也就不会进行更新。。。
*/
}
return watcher.value
}
}
}
总之,详见代码注释。。。
当computed依赖的data更新时
这里已经知道对data重新赋值,会触发其对应setter
// setter
set: function reactiveSetter (newVal) {
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
childOb = !shallow && observe(newVal)
dep.notify() // 这里会通知到所有watcher,让它们进行update
}
// dep.notify
notify () { // 在render阶段进行依赖收集时会将watcher加入subs列表,computed在进行计算的时候会收集依赖的data,
// 与此同时会将computed的watcher对象添加至data的dep.subs列表
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
render watcher和computed watcher执行update是不同的,computed watcher的update方法只会将watcher.dirty置为true,这代表该computed依赖的data发生了更新,需要重新计算;这样在render函数再次执行的时候会读取computed,触发computed的getter,在getter中会重新计算得出computed的新值,并且将dirty置为false,代表只需计算一次,在同一个render loop中多次引用该computed将不会重新计算。
THE END...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。