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

相关推荐
web1350858863528 分钟前
前端node.js
前端·node.js·vim
m0_5127446429 分钟前
极客大挑战2024-web-wp(详细)
android·前端
若川38 分钟前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
潜意识起点1 小时前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛1 小时前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿1 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@3 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
F-2H7 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++