计算属性原理
从源码看计算属性。分两块来讲解计算属性初始化流程
和计算属性watcher的触发流程
计算属性是定义在vm(vue实例)
上的一个特殊的getter
方法,他的getter
并不是用户提供的函数,而是vue
内部设置的代理函数。
计算属性初始化流程
在
beforcreate
到create
之间。
initComputed 初始化所有的计算属性
const computeWatcherOptions = { lazy : true }
function initComputed(vm,computed){
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRending()
for(const key in computed){
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if(process.env.NODE_ENV !== 'production' && getter !== null){
warn("...")
}
if(isSSR){
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computeWatcherOptions
)
}
if(!(key in vm)){
defineComputed(vm , key , userDef)
}else if(process.env.NODE_ENV !== 'production'){
if(key in vm.$data){
warn('...')
}else if(vm.$options.props && n vm.$options.props){
warn('...')
}
}
}
}
好了,我们一步一步来说。先说两点
noop
是一个空函数function(){}
这样,warn
是报错函数。vm
就是当前的Vue实例
,computed
就是当前实例上的所有计算属性。
const computeWatcherOptions = { lazy : true }
//computeWatcherOptions申明当前的Watcher是一个计算属性的Watcher
const wathcer = vm._computedWatchers = Object.create(null)
//会在vm实例上创建_computedWatchers,用来保存计算属性的Watcher依赖
开始遍历computed的所有计算属性,开始初始化。
//开始遍历computed的所有计算属性,开始初始化。
for(const key in computed){
const userDef = computed[key]
//userDef获取当前的计算属性的值
const getter = typeof userDef === 'function' ? userDef : userDef.get
//计算属性可以是函数也可以是对象,对象需要定义get和set属性。
if(process.env.NODE_ENV !== 'production' && getter == null){
warn("...")
}
//当不是开发环境并且getter为null时报错
...
}
例
computed:{
b : function(){
return this.a + 1
},
c : {
get :function (){
return this.a + 1
},
set :function (){
return this.a + 1
}
}
}
key 为 b时,userDef为function(){return this.a + 1},getter因为b是函数所以也是function(){return this.a + 1}
key 为 c时,userDef为{get:function(){...},set:function(){...}},getter因为c是对象所以是get:function(){...}
if(isSSR){
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computeWatcherOptions
)
}
服务端的话就不会生成计算属性的依赖,服务端也不会缓存数据,也不会通知。就是一个普通的getter,每次获取都要重新计算一遍。
生成Watcher时会将用户设置的函数传入依赖保存。后面可以用于重新计算数据。
if(!(key in vm)){
defineComputed(vm , key , userDef)
}else if(process.env.NODE_ENV !== 'production'){
if(key in vm.$data){
warn('...')
}else if(vm.$options.props && n vm.$options.props){
warn('...')
}
}
判断计算属性的key值是否已经在vm上了,如果存在data和props之上则报错计算属性失效。如果存在methods之上虽然不会报错,但是计算属性会默默失效。
不存在vm上的话开始对这个计算数据进入defineComputed
defineComputed把计算属性修改为存储描述符
懒得写源码了,这块直接说吧
const sharePropertyDefiniton = {
enumentable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed(target,key,userDef){
...
处理get和set
Object.defineProperty(target,key,sharePropertyDefiniton)
}
主要就是处理get和set的流程
函数
服务端
sharePropertyDefiniton.get = userDef || noot
sharePropertyDefiniton.set = noop || noot
客户端
sharePropertyDefiniton.get = createPropertyGetter(key) || noot
sharePropertyDefiniton.set = noop || noot
对象
服务端
sharePropertyDefiniton.get = userDef.get || noot
sharePropertyDefiniton.set = userDef.set || noot
客户端
sharePropertyDefiniton.get = createPropertyGetter(key) || noot
sharePropertyDefiniton.set = userDef.set || noot
最后如果set是noot的话set = function (){warn("...")},一个报错函数。然后设置Object.defineProperty。
createPropertyGetter
就是要说的第二部分,上面是计算属性的创建流程。createPropertyGetter
就是触发依赖流程。
createPropertyGetter触发依赖流程
这块比较抽象,createPropertyGetter是一个闭包函数。了解了这个就能了解计算属性的缓存结果和依赖触发机制。
function createPropertyGetter(key){
return PropertyGetter(){
const watcher = this._computedWatchers && this._computedWatchers[key]
if(watcher){
if(watcher.dirty){
watcher.evaluate()
}
if(Dep.target){
watcher.depend()
}
return watcher.value
}
}
}
好了,这里我们也分两块理解
缓存值
dirty为true时说明计算属性所用到的数据发生了变化,需要重新计算。
class watcher {
...
evaluate : function(){
this.value = this.get()
this.dirty = false
}
...
}
重新计算一遍,保存值。再把dirty修改为false
依赖
if(Dep.target){
watcher.depend()
}
class watcher {
...
depend : function(){
let i = this.deps.length
while(i--){
this.deps[i].depend()
}
}
...
}
computed:{
b : function(){
return this.a + 1
}
}
我们看上面的例子。
- 计算属性b如果被模板使用,b依赖于a
- a的依赖的dep中会有b的watcher和模板(组件watcher)
- 当a发生变化会通知b把dirty修改为true,同时通知模板(组件watcher)开始渲染流程
- 模板读取计算属性b,重新计算b的值用于渲染
我们来说说a是什么时候收集到
计算属性的wathcer
当计算值时会触发a的get,这时候a就收集了计算属性b的依赖了。
模板的wathcer
调用计算属性b时,b中有一个deps保存了所有依赖数据的watcher。b的deps中就有[a的wathcer]。触发所有依赖的depend,就可在a中收集模板的依赖。
if(Dep.target){
watcher.depend()
}
这就是它的作用
问题
这个模式也有一个问题,计算结果哪怕与上次相同也还是会是模板重新进入渲染流程。所有在2.5.17版本后修改了defineComputed。惊不惊喜,开不开心。
function createPropertyGetter(key){
return PropertyGetter(){
const watcher = this._computedWatchers && this._computedWatchers[key]
if(watcher){
watcher.depend()
return watcher.evalute()
}
}
}
watcher中
evalute:function(){
if(this.ditry){
this.value=this.get()
this.dirty=false
}
return this.value
}
depend:function(){
if(this.dep && Dep.target){
this.dep.depend()
}
}
- 模板使用计算属性,计算属性将组建watcher保存到dep.subs
- 当数据发生变化时通知计算属性
- 先触发计算属性中的update,这里判断dep.subs中是否有依赖
- 没有依赖将dirty改为true
- 有依赖重新计算值,如果不同调用dep.subs中所有的依赖通知他们计算属性发生了变化