computed和watch的区别
相同点:两者都是观察页面数据变化的
不同点:
- computed只有当依赖的数据变化时才回去计算,否则只会从缓存中去取
- watch每次都需要执行函数,更适用于数据变化的异步操作
computed的基本原理和源码实现
computed的设计初衷是:为了使模板中的逻辑运算更加简单
- 可以使模板中的逻辑更加简单清晰,方便代码管理
- 可以使用缓存,只有当依赖的数据发生变化的时候,才会重新计算
computed的初始化
寻找初始化函数(定义)
通常在我们的项目中的main.js
中,通过 new Vue({})
来实例化代码,
- 调用的就是
vue/src/core/instance/index.js
里面的_init()
方法,_init()
是在initMixin()
方法中的 _init()
中调用了initState()
进而在存在opts.computed
调用了initComputed(vm, opts.computed)
方法,这里我们就找到了初始化computed
的方法了
初始化函数中都写了啥
- 首先使用
Object.create(null)
; 创建一个空对象, 分别赋值给watchers
和vm._computedWatchers
;
- 这是在实例上定义 _computedWatchers 对象,用于存储 ’计算属性Watcher‘
-
因为
computed
可以写成一个函数或者一个对象,所以,在接下来我们是使用for in
循环遍历,computed
中的每一个值,同时判断是函数声明还是对象声明,来获取每个计算属性的getter
-
接下来是为每个计算属性,根据
key
来实例化对应的Watcher
,存在上面创建的watchers
中,getter
作为其中的一个参数-
创建
计算属性Watcher
,getter
作为参数传入,他会在依赖属性更新的时候调用,并对计算属性重新取值,需要注意里面的lazy
配置,那是实现缓存的标识 -
可以理解为
computed
中的每一个key
对应的值,都是一个Watcher
的实例,通过发布订阅模式来监听的
-
-
说到了
lazy
配置和用到了Watcher
构造函数,那么我们就要去查看Watcher
的实现-
在第三步中
lazy
是通过第四个参数传入的const computedWatcherOptions = { lazy: true };
-
Watcher
的constructor
一共有五个参数,第三部传入了四个,这里我们关注点在第四个参数我们传入了options = {lazy: true};
this.vm = vm // 这是Watcher构造函数中,接下来的代码 vm._watchers.push(this) // 这是把当前的 watcher 添加到vue实例上 // 上面的this 就是 state.js 对应的watchers[key],就是第三步中创建的 // 这时候vm._watchers.vm 就是我们的computed所在的组件的实例,就是整个组件的vm // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy // 这里根据传参的值来给`Watcher`对应的静态属性赋值 // lazy用于标记watcher是否为懒执行,该属性是给 computed data 用的,当 data 中的值更改的时候,不会立即计算 getter // 获取新的数值,而是给该 watcher 标记为dirty,当该 computed data 被引用的时候才会执行从而返回新的 computed // data,从而减少计算量。 this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // 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() : ''
后面还有不是懒加载类型会调用
this.get()
方法,这里我们不去扩展 -
执行到这里
state.js
中的initComputed
中的下面这段代码算是执行完毕了watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions )
-
-
接下来判断,如果
computed
中的key没有在vm
中, 则通过defineComputed
挂载上去。在初始化阶段,因为是第一次执行, 所以vm
中是没有该属性的,所以初始化的时候,必然会调用defineComputed
对数据进行劫持(就是变为响应式)-
defineComputed
这个函数逻辑很简单,就是判断是不是服务端渲染,不是的话则为true,赋值给shouldCache
变量,作用就是判断是否需要被缓存,意思是只要不是服务端渲染都是默认要缓存的 -
通过传过来的值
userDef
来判断这个computed[key]
是函数式声明还是对象式声明,使用不同的方法 来个state.js
最上方声明的变量sharedPropertyDefinition
的set和get赋值,然后通过// 数据劫持 使之变为响应式 Object.defineProperty(target, key, sharedPropertyDefinition)
使之变为响应式
-
-
客户端渲染这边赋值的方法是通过
createComputedGetter
因此我们给
sharedPropertyDefinition.get
赋值的就是createComputedGetter
函数return出来的computedGetter
函数function createComputedGetter (key) { // 核心 return function computedGetter () { // 上面的 initComputed 函数中 ’计算属性Watcher‘就存储在 实例的 _computedWatchers 中,这里相当于是取出来 const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { // 这个就是缓存的触发,这里就是不能用缓存的了 要重新计算 watcher.evaluate() // 计算属性重新求值 } if (Dep.target) { // Dep对象有,就要收集依赖 watcher.depend() } return watcher.value // 返回的计算属性的值 } } }
最后我们来总结一下,在页面第一次初始化的时候,我们是如何初始化执行的呢:
- 当我们执行
_init()
函数时,会调用vm.$mount
,这段代码的作用就是对页面的模板进行编译操作,这不是我们这里关注的点,暂时略过,我们只关注下面这段代码
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
-
vm.$mount
的定义是在vue/src/platforms/web/entry-runtime-with-compiler.js
中这里因为有一段代码
const mount = Vue.prototype.$mount
把之前的Vue.prototype.$mount
方法赋值给了mount
,然后在这个文件中重写了Vue.prototype.$mount
方法,并在这个重写方法的最后调用了mount.call(this, el, hydrating)
-
而初始的
Vue.prototype.$mount
是定义在vue/src/platforms/web/runtime/index.js
中的,那么上一步的调用mount.call(this, el, hydrating)
到最后就是调用了mountComponent
方法 -
mountComponent
中,通过new Watcher
对Watcher
实例化了,这里我们又要去到watcher.js
中new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
-
因为实例化的时候第四个参数没有传入
lazy
,所以需要调用this.get()
方法,这个时候this.lazy = false
this.value = this.lazy ? undefined : this.get();
- 在
this.get()
中会执行value = this.getter.call(vm, vm)
,这里的this.getter
就是我们定义的computed[key]
对应的值,那就走到了下面
function createComputedGetter (key) { // 核心
return function computedGetter () {
// 上面的 initComputed 函数中 ’计算属性Watcher‘就存储在 实例的 _computedWatchers 中,这里相当于是取出来
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) { // 这个就是缓存的触发,这里就是不能用缓存的了 要重新计算
watcher.evaluate() // 计算属性重新求值
}
if (Dep.target) { // Dep对象有,就要收集依赖
watcher.depend()
}
return watcher.value // 返回的计算属性的值
}
}
}
- 最后返回的就是
watcher.value
的值,就是计算属性的值了,整个computed
的执行过程就是通过事件的发布订阅模式来监听对象数据变化实现的