vue2源码学习--07计算属性computed

computed主要要注意的有两点:

1、通过所谓的脏值检测,来保证多次使用该值但是只执行一次里边的回调函数并记录该返回值

2、依赖收集和更新,计算属性可能依赖多个值,需要保证只要依赖有变化就要去重新执行计算函数

js 复制代码
export function initState(vm) {
    const opts = vm.$options; // 获取所有选项
    if(opts.data) {
        initData(vm)
    }
    // 开始computed
    if(opts.computed) {
        initComputed(vm)
    }
}

// vm上维护一个变量_computedWatchers 用来存放所有computed的副作用函数
function initComputed(vm) {
    const computed = vm.$options.computed 
    const watchers = vm._computedWatchers = {} 
    for (let key in computed) {
        let userDef = computed[key]
        // 我们需要监控计算属性中get的变化
        // computed写法有两种 一种是函数,一种是对象{set, get}
        let fn = typeof userDef === 'function'? userDef: userDef.get
        // 执行的副作用函数是通过watcher类来实现的,通过第三个参数来作为标示,区别渲染watcher
        watchers[key] = new Watcher(vm, fn, {lazy:true})
        // 这一步是跟data做一样的操作,通过this直接访问computed属性
        defineComputed(vm, key, userDef)
    }
} 

function defineComputed(target, key, userDef) {
    const setter = userDef.set || (() => {})
    // conmputed 上的属性可以用this访问
    Object.defineProperty(target, key, {
        get: createComputedGetter(key), // 这个函数就是执行_computedWatchers对应key的watcher里的方法
        set: setter
    })
}

读取到计算属性时执行createComputedGetter,先说下这个函数的思路,这里可以拿到保存的计算属性watcher,在计算属性watcher里我们可以做下修改,通过第三个参数判断如果是计算属性就先不执行第二个参数(传进来的函数)而是重写一个方法evaluate专门给计算属性去调用get,然后通过一个变量第一次进来可以执行,之后将变量置反保证后续不再重复执行。

先修改下watcher

js 复制代码
class Watcher {
    constructor(vm, fn, options) {
        this.id = id++
        this.renderWatcher = options
        this.getter = fn
         
        this.deps = [] // 后续实现计算属性 和清理工作
        this.depsId = new Set()
        this.vm = vm
        // 计算属性标示
        this.lazy = options.lazy
        this.dirty = this.lazy // 缓存值
        this.lazy? undefined: this.get()
    }
    get() {
        Dep.target = this // 调用的时候把当前watcher赋值给Dep.target
        let value = this.getter.call(this.vm) // 会去vm上取值
        Dep.target = null // 渲染完成置空
        return value // 把返回值暴露出来
    } 
    addDep(dep) { //一个组件对应多个属性 ,重复属性不用记录 
        let id = dep.id
        if(!this.depsId.has(id)) {
            this.deps.push(dep)
            this.depsId.add(id)
            dep.addSub(this) // watcher已经记住dep了而且去重 此时让dep也记住watcher
        }
    }
    evaluate() {
        this.value = this.get() // 获取用户的返回值 并且标记为脏
        this.dirty = false  
    }
    update() { 
        queueWatcher(this)
    }
    run() {
      this.get()
    }

}

完成createComputedGetter函数

js 复制代码
function createComputedGetter( key) {
  // 需要检测是否执行getter
  return function() {
      const watcher = this._computedWatchers[key]
      if(watcher.dirty) {
          // 如果是脏的就去执行 用户传入的函数
          watcher.evaluate()
      }
      return watcher.value
  }
}

检验下成果

看到虽然页面上两次使用fullName但是计算属性的函数只执行一次,到此computed写完一半了,即读取的时候有了脏值检测并进行缓存结果,剩下的就是更新了。

更新这个很复杂,我们顺着代码执行顺序开始捋。

1、进入页面执行渲染watcher

2、按之前的写法将当前渲染watcher赋值给Dep.target。

3、render函数读取值fullName,执行evaluate,将计算watcher赋值给Dep.target

4、读取到依赖firstName,lastName,因为Dep.target有值所以会进行depend,但是此时subs收集到的为计算watcher,deps会收集到两个dep。如果依赖更新只会通知计算watcher去执行,而不会进行视图更新。

问题找到了,所以目前要做的就是要改下Dep.target的赋值规则,在先生成渲染watcher后又生成计算watcher时,Dep.target后者不要覆盖前者,而是维护一个栈,先入后出,当后一个退出后重新赋值前一个,并在此时如果当前Dep.target还有值就认为是渲染watcher让subs重新进行一次收集watcher即收集到渲染watcher。

dep.js 暴露出两个方法

js 复制代码
Dep.target = null
let stack = []
export function pushTarget(watcher) {
    stack.push(watcher)
    Dep.target = watcher
}
export function popTarget(watcher) {
    stack.pop()
    Dep.target = stack[stack.length -1]
}

watcher.js

js 复制代码
class Watcher {
    constructor(vm, fn, options) {
        this.id = id++
        this.renderWatcher = options
        this.getter = fn
         
        this.deps = [] // 后续实现计算属性 和清理工作
        this.depsId = new Set()
        this.vm = vm
        // 计算属性标示
        this.lazy = options.lazy
        this.dirty = this.lazy // 缓存值
        this.lazy? undefined: this.get()
    }
    get() {
        pushTarget(this) // 调用的时候把当前watcher赋值给Dep.target
        let value = this.getter.call(this.vm) // 会去vm上取值
        popTarget(this) // 渲染完成置空
        return value
    } 
    // 当执行完计算属性的getter,重新赋值Dep.target后再调用此方法 让之前收集的dep收集一次watcher
    depend() {
        let i = this.deps.length
        while(i--) {
            this.deps[i].depend() //  
        }
    }
    // 更新的时候计算watcher只用放开dirty锁就行 渲染watcher会重新读一遍计算属性
    update() { 
      if(this.lazy) {
          this.dirty = true
      } else {
          queueWatcher(this)
      }
    }
}

完善createComputedGetter

js 复制代码
function createComputedGetter( key) {
  // 需要检测是否执行getter
  return function() {
      const watcher = this._computedWatchers[key]
      if(watcher.dirty) {
          // 如果是脏的就去执行 用户传入的函数
          watcher.evaluate()
      }
      // 计算watcher的getter执行完成后 如果还有Dep.target就再进行一次依赖收集
      if(Dep.target) {
          watcher.depend()
      }
      return watcher.value
  }
}

验证下效果

总算完成了,computed应该算vue2最难理解的一部分了,下一篇写个简单的watch

相关推荐
小镇程序员11 分钟前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐13 分钟前
前端图像处理(一)
前端
程序猿阿伟20 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒22 分钟前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪31 分钟前
AJAX的基本使用
前端·javascript·ajax
力透键背34 分钟前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M44 分钟前
node.js第三方Express 框架
前端·javascript·node.js·express
盛夏绽放1 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
想自律的露西西★1 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5
白墨阳1 小时前
vue3:瀑布流
前端·javascript·vue.js