浅浅细聊vue源码实现computed流程

171 阅读3分钟

1.背景

掘金中介绍vue中computed的实现原理的的文章多且精,大家都觉得这个computed的实现比较BT,本文就computed实现过程中的渲染watcher和_computedWatcher如何出入栈的过程详细,深刻理解computed中的属性如何收集到渲染watcher,也是解决自己当时学习源码时的疑问,新人文章不对的老哥们尽管提。

2.简述vue渲染过程

2.1 初始化

众所周知vue初始化就是要new个Vue

    const vm = new Vue({
            data() {
                return {
                    a: '我', b: '妻',c: '善',d: '逸',
                }
            },
            computed:{
                myName(){
                    return this.a+this.b +this.c+this.d
                }
            }
           
        })

这个过程发生了什么呢,vue2中使用Object.definePorperty给每个属性添加get和set函数,其中涉及的数据包含data中的a,b,c,d和computed属性的myName,于此同时_computedWatchers诞生了,_computedWatchers用于存放computed属性所创建的观察者watcher,这里的watcher也就是实现vue响应式原理的核心,不理解的小伙伴可以去细看vue响应式原理,会有订阅类和观察者类,有助于理解本文,但是不关键,这里仅贴watcher类里的部分核心方法

class Watcher {
    constructor(vm, fn, options,cb) {
        this.id = id++
        this.vm = vm
        this.renderWatcher = options
        if(typeof fn === 'string'){
            this.getter = function(){ return vm[fn]}
        }else{
            this.getter =  fn
        }
        this.cb = cb
        this.lazy = options?.lazy
        this.dirty = this.lazy
        //初始化watcher类是lazy为true不执行get方法
        this.value = this.lazy ? undefined : this.get()
    }
    //取值执行回调
    get() {
        pushTarget(this)
        let value = this.getter.call(this.vm)
        popTarget()
        return value
    }
    //computed属性
     evaluate() {
        this.value = this.get()
        this.dirty = false
    }
    }
let stack = [] //用来管理watcher的栈
//入栈
 function pushTarget(watcher){
    Dep.target = watcher
    stack.push(watcher)

}
//出栈
function popTarget(){
    stack.pop()
    Dep.target = stack[stack.length-1]

}

​ //初始化computed的属性

function initComputed(vm) {
    const computed = vm.$options.computed
    const wathcers = vm._computedWatchers = {}
    //遍历computed属性
    for (let key in computed) {
        let userref = computed[key]
        let fn = typeof userref == 'function' ? userref : userref.get
        //new一个watcher类,属性为lazy并不执行get的回调方法
        watchers[key] =  new Watcher(vm, fn, { lazy: true })
        
        defineComputed(vm, key, userref)
    }
   
}

function defineComputed(vm, key, userref) {
    const getter = typeof userref == 'function' ? userref : userref.get
    const setter = userref.set
    //给computed属性添加自定义get方法
    Object.defineProperty(vm, key, {
        get: creatComputedGetter(key),
        set: setter
    })
}

function creatComputedGetter(key){
    return function (){
        const watcher =  this._computedWatchers[key]
        if(watcher.dirty){
            //触发计算 即wathcer中的get方法
            watcher.evaluate()
        } 
        if(Dep.target){
            //当时不理解为什么此时渲染watcher还存在 能被收集到
            //让myName的watcher的订阅者收集到渲染watcher
                watcher.depend()
        }   
        return watcher.value
    }
}
2.2 元素挂载

元素挂在就是我们熟知的vue.$mounted('#app'),

    Vue.prototype.$mount = function (el) {
        const vm = this;
    //省略很多代码 下面这个方法是vue通过模板编译成AST语法树 
         const render = compileToFunction(template)
                options.render = render
                  mountComponent(vm, el)
    }
    function mountComponent(vm,el){
    vm.$el = el 
    const updateComponet = ()=>{
        vm._update(vm._render())
       
    }
  	//此时渲染watcher
    let watcher = new Watcher(vm,updateComponet)
    
}

3.渲染watcher和computedWatcher的交融

看完以上代码,思考两个问题:

1.渲染watcher和computedWatcher谁先被创建的?

2.computedWatcher中的myName的收集到渲染watcher(本文立意)

​ 其实很明显,new Vue的过程中数据进行初始化,这个时候computed的属性也被初始化,就每个computed的属性都会创建一个watcher被_computedWatchers统一收集,并绑定自定义的get方法和set方法,所以computedWatcher先被创建了,并追加了lazy为true的属性,回头看代码,lazy为true就不会执行get方法,所以不会出入栈,这个时候不会出入栈,那么什么时候computedWatcher入栈的呢,这是我当时在学习转不过来弯的地方

​ 数据初始化完成之后,到了挂在元素的时候,就会触发新建一个渲染watcher,它为什么叫渲染watcher,因为传入的回调函数,是触发渲染更新的函数vm._update(vm._render()),这也是为什么computedWatcher也要收集这个渲染watcher的原因,因为需要触发视图更新。渲染watcher新建时没有设置lazy参数,所以会执行watcher的get方法,此时渲染watcher入栈了,接下来执行了回调vm._update(vm._render()),vm._render()渲染函数会取视图中需要的变量值,也就是myName,因为在模板编译过程中会解析这个属性,myName的属性在初始化的时候已经自定义了其get方法,所以这个时候会触发watcher.evaluate()这个计算方法,计算方法中会触发当前myName的watcher的出栈以及入栈,那么此时栈中还是存在一个渲染watcher的,myName的watcher的订阅者此时就可以收集到渲染watcher了,之后渲染wactcher出栈,功成身退,从而在实现computed的计算属性。

image.png