Vue2.0 computed源码分析

目录

  • 一. computed初始化过程
  • 二. computed依赖的属性发生变化页面是如何重新渲染的

一. computed初始化过程

vue进行页面渲染首先经过属性的初始化,在创建render函数最后进行挂载阶段。

在进行属性初始化的时候,会对计算属性computed进行初始化,主要代码如下:
Vue2.0 computed源码分析_第1张图片
1.计算属性可以有两种写法一种是函数形式一种是对象形式,对象格式是有get和set属性。
2.对于每一个属性创建一个对应的watcher,并在vue实例对象上面创建_computedWatchers属性用来存放对应计算属性的watcher,并设置参数lazy:true;这个参数用于告诉watcher我是个计算属性watcher以及赋值给watcher实例dirty属性用来控制计算属性的缓存功能。
由于我们每次调用计算属性都会返回一个值,所以计算属性也是需要使用Object.defineProperty来进行改写,主要代码如下
Vue2.0 computed源码分析_第2张图片
接下来就是改写计算属性的get方法。
Vue2.0 computed源码分析_第3张图片
每次获取计算属性的值的时候,会调用get方法,在get方法中我们获取到对应的计算属性watcher,如果当前计算属性的watcher存在则判断watcher的dirty是都是脏的,如果是脏的那么可以对当前的watcher进行求值。我们来看计算属性的watcher初始化以及evaluate方法。

class Watcher {
  constructor(vm,exprOrFn,cb,options){
    this.vm= vm;
    this.exprOrFn = exprOrFn;
    this.cb = cb;
    this.options = options
    this.deps = []
    this.depsId = new Set()
    this.user = options.user || false; // 这是一个用户watch
    this.lazy = options.lazy; // lazy不可变 如果watch上有lazy属性说明是计算属性
    this.dirty = this.lazy; // dirty代表取值时,是否执行用户提供的方法exprOrFn
    this.id = id++; // watcher的唯一标识
    if(typeof exprOrFn == 'function'){
      this.getter = exprOrFn;
    }else{
      // 自定义watcher exprOrFn是个key
      /**
       * exprOrFn 可能传递过来的是一个字符串a
       */
      this.getter = function(){
        // 当去当前实例取值的时候 才会触发依赖手机
        let path = exprOrFn.split('.');
        let obj = vm;
        for(let i = 0;i < path.length;i++){
         obj = obj[path[i]]
        }
        return obj;
      }
    }
    // if(this.lazy)
    // 默认先调用一次get方法,进行取值 将结果保留下来
    this.value = this.lazy ? void 0 : this.get() // 默认会调用get方法
    // console.log(' this.value', this.value)
  }
  addDep(dep){
    // 一个组件watcher或者其他watcher会存放很多dep
    let id = dep.id;
    if(!this.depsId.has(id)){
      // 对于一个watcher  没有重复的dep 就相当于dep中没有重复的watcher
      this.deps.push(dep)
      this.depsId.add(id)
      dep.addSub(this)
    }
  }
  get(){
    // 当前watcher的实例
    // Dep.target增加了当前的watcher实例 然后渲染页面  然后调用get方法
    //computed: 在页面进行初始化的时候,会创建渲染watcher,这时候会将渲染 watcher存放到Dep的target上面接着获取模板上面相关的属性值,这时候比如会调用fullName的Object.defineProperty里面的get方法,第一次调用计算属性的get方法会调用相关的计算属性的watcher这时候dirty为true会接着调用计算属性相关联的属性,这时候会将当前的计算属性watcher存放到Dep.target中,这时候stack栈中会有两个watcher
    pushTarget(this)
    // 对于计算属性来说  这个this.getter对应的是watcher实例调用this.getter的时候函数内部的this永远指向的是watcher实例招不到对应的依赖属性 如 this.firstName 和 this.lastName
    // console.log('方法',this.getter)
    let result =  this.getter.call(this.vm) // 默认会调用 exprOrFn 这里是渲染页面 需要取值 render 方法(with(vm))
    // console.log('result',result)
    console.log('存放的watcher',this)
    popTarget()
    console.log('删除对应的watcher')
    return result;
  }
  update(){ 
    if(this.lazy){ // 是计算属性  如果计算属性相关联的属性发生了变化那么将dirty设置为true下次获取计算属性的时候可以调用对应的evaluate求值方法
      this.dirty = true;
    }else{
      // 这里不要么看次都调用get方法  get方法会重新渲染页面
      // 先把当前watcher的get缓存起来
      queueWatcher(this) // 暂存的概念
      // this.get()
    }
  }
  // 求值  调用get
  evaluate(){
    // dirty 当脏值为true 的时候获取computed的值,会调用对应的watcher的evaluate方法,这个方法会调用get方法,get方法会调用computed里面的函数(get)函数,改函数会调用对应依赖的属性,此时会将依赖的属性上面增加对应的computedwatcher
    this.value = this.get();
    this.dirty = false // 取过一次值
  }
  run(){ 
   let newValue  = this.get(); 
   let oldValue = this.value; // 老值
   // 当新的值和老的值一样 在设置的时候已经进行了拦截
   if(this.user){
    this.cb.call(this.vm,newValue,oldValue)
    // 更新一下老的值
    this.value = newValue
   }
  }
  depend(){
    // 计算属性watcher 会存储 dep dep会存储watcher
    let i = this.deps.length; // 计算属性的dep都取出来 让依次的dep存放当前的渲染watcher
    while(i--){
      this.deps[i].depend(); // 让计算属性相关的依赖属性dep存放渲染watcher这时候每个依赖的属性的dep当中都有一个computed计算属性watcher和渲染watcher,当属性发生变化的时候先调用计算属性的watcher的update将dirty设置为true,然后调用渲染watcher的update重新渲染页面
    }
  }
}

计算属性watcher初始化的时候会有个默认的options,options就是刚才说的lazy:true,然后将lazy赋值给dirty:true,lazy只是标志这是个计算属性的watcher,而dirty是真正控制是否调用watcher的get方法的。在初始化的时候判断是否是计算属性watcher如果是 this.value = this.lazy ? void 0 : this.get() 那就默认不调用get方法。当获取计算属性的时候默认dirty为true,那么调用计算属性的evaluate方法

 evaluate(){
    // dirty 当脏值为true 的时候获取computed的值,会调用对应的watcher的evaluate方法,这个方法会调用get方法,get方法会调用computed里面的函数(get)函数,改函数会调用对应依赖的属性,此时会将依赖的属性上面增加对应的computedwatcher
    this.value = this.get();
    this.dirty = false // 取过一次值
  }
   get(){
    // 当前watcher的实例
    // Dep.target增加了当前的watcher实例 然后渲染页面  然后调用get方法
    //computed: 在页面进行初始化的时候,会创建渲染watcher,这时候会将渲染 watcher存放到Dep的target上面接着获取模板上面相关的属性值,这时候比如会调用fullName的Object.defineProperty里面的get方法,第一次调用计算属性的get方法会调用相关的计算属性的watcher这时候dirty为true会接着调用计算属性相关联的属性,这时候会将当前的计算属性watcher存放到Dep.target中,这时候stack栈中会有两个watcher
    pushTarget(this)
    // 对于计算属性来说  这个this.getter对应的是watcher实例调用this.getter的时候函数内部的this永远指向的是watcher实例招不到对应的依赖属性 如 this.firstName 和 this.lastName
    // console.log('方法',this.getter)
    let result =  this.getter.call(this.vm) // 默认会调用 exprOrFn 这里是渲染页面 需要取值 render 方法(with(vm))
    // console.log('result',result)
    console.log('存放的watcher',this)
    popTarget()
    console.log('删除对应的watcher')
    return result;
  }
  Dep.target = null; // 类的静态属性
let stack = [];  // 存放watcher的栈
export function pushTarget(watcher){
  // Dep.target就是一个承载  承载当前的watcher实例
  Dep.target = watcher
  stack.push(watcher) // 有渲染watcher
  console.log('stack',stack)
}
/**
 * 分析:第一次存放的是渲染watcher  然后调用get方法,然后接下来又开始调用计算属性的get方法,evaluate方法中调用get方法,这个方法也会讲对应的计算属性watcher存放进去然后,继续进行下一行popTarget方法就是删除当前栈顶的watcher,即当前的计算属性watcher
 */
export function popTarget(){
  // Dep.target就是一个承载  承载当前的watcher实例
  // 删除当前的watcher实例
  // Dep.target = null
  stack.pop();
  Dep.target = stack[stack.length - 1]; // 设置当前的Dep的静态属性target,为现在的渲染watcher,如果发现栈中还有watcher那么就将当前的渲染watcher放入的相应的dep中
}

evaluate方法会调用watcher的get方法,get方法第一行是将Dep的静态属性存放但当前的watcher,第二行调用计算属性的get方法默认会获取计算属性相关依赖属性比如

fullName(){
        console.log('回执行吗')
        // this.firstName , this.lastName求值时候,会记住计算属性的watcher,当这些属性发生改变的时候,会调用对应的computed的watcher去重新获取computed的值
        return this.firstName + this.lastName
      },

1.这时候会将firstName和lastName属性的dep中存放当前的计算属性watcher。
2.get调用完成以后会将当前的computed属性值给当前的watcher的属性value,并返回给get方法。这时候初始化就算完成了。
3.当相关的属性发生改变的时候如firstName和lastName改变的时候,会调用dep的notify方法,调用相关依赖的watcher的update方法

 update(){ 
    if(this.lazy){ // 是计算属性  如果计算属性相关联的属性发生了变化那么将dirty设置为true下次获取计算属性的时候可以调用对应的evaluate求值方法
      this.dirty = true;
    }else{
      // 这里不要么看次都调用get方法  get方法会重新渲染页面
      // 先把当前watcher的get缓存起来
      queueWatcher(this) // 暂存的概念
      // this.get()
    }
  }

判断如果是计算属性的话,那么重新将dirty设置为true,表示脏值为true,下次再获取计算属性的时候,会重新调用计算属性的evalute方法,重新获取计算属性的值。

我们把computed初始化讲完以后我们开始讲解当依赖的属性发生变化,页面是怎么重新渲染的。

二. computed依赖的属性发生变化页面是如何重新渲染的

这要从vue是怎么进行渲染页面的开始说起,vue挂载的时候会生成一个渲染watcher

export function mountComponent(vm,el){
  // 调用render方法去渲染 el属性
  // 先调用render方法创建虚拟节点,再讲虚拟节点渲染到页面上
  //vm._render() 创建虚拟节点  vm._update将虚拟节点创建成真实的节点
  callHook(vm,'beforeMount')
  let updateComponent = ()=>{
    vm._update(vm._render())
  }
  // 这个watcher是用于渲染的 目前没有任何功能 updateComponent()
  // 初始化渲染阶段 将Dep.target 赋值为当前的watcher 渲染结束以后Dep.target设置为空
  new Watcher(vm,updateComponent,()=>{
    callHook(vm,'updated')
  },true) // 渲染watcher标志
  
  // 要把属性和watcher绑定在一起
  callHook(vm,'mounted')
}
 
 get(){
    pushTarget(this)
    let result =  this.getter.call(this.vm) 
    popTarget()
    return result;
  }
  1. 渲染watcher会先调用get方法,同样也会调用pushTarget方法,这次是将watcher存放到stack栈中2.
  2. 接着执行下一行调用getter方法,这个getter方法就是调用初始化生成的render方法,render方法是解析vue模板,生成_render函数,这个函数中会获取模板中的相关属性,比如计算属性fullName,调用fullName的get方法,同样也会进入到计算属性的watcher中,也会调用这个get方法,
  3. 接着计算属性的watcher入栈,执行getter方法以后,出栈,这时候Dep.target是当前的渲染watcher,继续执行计算属性的get方法,判断 如果有Dep.target,那么调用watcher的depend方法。
depend(){
    // 计算属性watcher 会存储 dep dep会存储watcher
    let i = this.deps.length; // 计算属性的dep都取出来 让依次的dep存放当前的渲染watcher
    while(i--){
      this.deps[i].depend(); // 让计算属性相关的依赖属性dep存放渲染watcher这时候每个依赖的属性的dep当中都有一个computed计算属性watcher和渲染watcher,当属性发生变化的时候先调用计算属性的watcher的update将dirty设置为true,然后调用渲染watcher的update重新渲染页面
    }
  }

这个方法会给计算属性相关依赖属性如firstName和lastName增加当前的渲染watcher,所以当我们改变firstName和lastName 的时候,会执行两种watcher一个是计算属性watcher一个是渲染watcher。这时候页面重新渲染。

你可能感兴趣的:(Vue2.0 computed源码分析)