从零开始的Vue世界-04

570 阅读6分钟

本章主要解释Vue中计算属性computed和watch的原理

computed

计算属性,即当他依赖的属性发生变化时才会变化(计算属性的结果会被缓存,除非依赖的响应式变化才会重新计算)

  • index.html
 <script>
        const vm = new Vue({
            el:'#app',
            data:{
                firstname:'张',
                lastname: '三',
                age:13
            },
            computed:{
                newName(){
                    return this.age
                },
                fullname(){
                    console.log('run')
                    return this.firstname + this.lastname
                }
            }
        });
        
        setTimeout(()=>{
            vm.firstname = 'xxx'; 
        },1000)
    </script>
  • 在init.js中,进行computed计算属性的初始化
import Watcher from './observe/watcher'

export function initState(vm) {
    const opts = vm.$options; // 获取所有的选项
    if (opts.data) {
        initData(vm);
    }
    if (opts.computed) {
        initComputed(vm);
    }
}
function initComputed(vm) {
    const computed = vm.$options.computed;
    const watchers = vm._computedWatchers = {}; // 将计算属性watcher保存到vm上
    for (let key in computed) {
        let userDef = computed[key];

        // 我们需要监控 计算属性中get的变化
        let fn = typeof userDef === 'function' ? userDef : userDef.get

        // 如果直接new Watcher 默认就会执行fn, 将属性和watcher对应起来 
        watchers[key] = new Watcher(vm, fn, { lazy: true })

        defineComputed(vm, key, userDef);
    }
}

function defineComputed(target, key, userDef) {
    // const getter = typeof userDef === 'function' ? userDef : userDef.get;
    const setter = userDef.set || (() => { })

    // 可以通过实例拿到对应的属性
    Object.defineProperty(target, key, {
        get: createComputedGetter(key),
        set: setter
    })
}

// 计算属性根本不会收集依赖 ,只会让自己的依赖属性去收集依赖
function createComputedGetter(key) {
    // 我们需要检测是否要执行这个getter
    return function () {
        const watcher = this._computedWatchers[key]; // 获取到对应属性的watcher
        if (watcher.dirty) {
            // 如果是脏的就去执行 用户传入的函数
            watcher.evaluate(); // 求值后 dirty变为了false ,下次就不求值了
        }
        if (Dep.target) { // 计算属性出栈后 还要渲染watcher, 我应该让计算属性watcher里面的属性 也去收集上一层watcher
            watcher.depend();
        }
        return watcher.value; // 最后返回的是watcher上的值
    }
}
  • Watcher
class Watcher { // 不同组件有不同的watcher   目前只有一个 渲染根实例的
    constructor(vm, fn, options) {
        this.id = id++;
        this.renderWatcher = options; // 是一个渲染watcher
        this.getter = fn; // getter意味着调用这个函数可以发生取值操作
        this.deps = [];  // 后续我们实现计算属性,和一些清理工作需要用到
        this.depsId = new Set();
        this.lazy = options.lazy;
        this.dirty = this.lazy; // 缓存值
        this.vm = vm;
        this.value = this.lazy ? undefined :  this.get();
       
    }
    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;
    }
    get() {
        pushTarget(this)// 静态属性就是只有一份
        let value = this.getter.call(this.vm); // 会去vm上取值  vm._update(vm._render) 取name 和age
        popTarget() // 渲染完毕后就清空
        return value;
    }
    depend(){ // watcher的depend 就是让watcher中dep去depend
        let i =  this.deps.length;
        while(i--){
            // dep.depend()
            this.deps[i].depend(); // 让计算属性watcher 也收集渲染watcher
        }
    }
    update() {
        if(this.lazy){
            // 如果是计算属性  依赖的值变化了 就标识计算属性是脏值了
            this.dirty = true;
        }else{
            queueWatcher(this); // 把当前的watcher 暂存起来
            // this.get(); // 重新渲染
        }
    }
    run() {
        let oldValue = this.value;
        let newValue = this.get();  // 渲染的时候用的是最新的vm来渲染的
        if(this.user){
            this.cb.call(this.vm,newValue,oldValue);
        }
    }
}

computed流程分析

new Vue({
    ...,
    computed:{
        fullname(){ // defineProperty中的get方法
            console.log('run')
            return this.firstname + this.lastname
        }
    }
});

首先进行了初始化状态,执行了initState(),这哥函数里会执行判断有没有computed,有的话执行计算属性的初始化initComputed()

  1. initComputed会实例vm上维护一个对象_computedWatchers,用来存放接下来创建的watcher,遍历computed,遍历过冲中,创建一个变量记录get函数fn,即计算属性的get,当计算属性是一个函数时,fn就是该函数,当他不是函数,说明是对象类型,取对象的get函数;然后new Watcher()传入实例,刚才的fn,和一个{lazy:true}

  2. 在new Watcher()的过程中,把传入的fn赋值给watcher的getter,判断传入的options的lazy(lazy为true不执行get()),当执行evaluate时调用get,get又会执行this.getter,当前的watcher(Dep.tag)入栈当前的计算属性watcher(this),然后就会执行传入的fn函数(function(){return this.firstname + this.lastname})这时执行了data数据中的get方法,讲当前watcher依赖收集到dep中,这时watcher中就有两个dep,dep中也有计算属性的watcher,执行完后出栈,计算属性的watcher去掉,然后将值赋值给当前watcher的value.

  3. 在第二步遍历过程最后一步,会进行一个数据劫持,这样就可以this.computed[key]取值,执行defineComputed,进行数据劫持,setter为用户写的函数或者空函数,getter为当前fn的值,但是直接这样写会有问题,执行多次的问题,但只需要执行一次,封装一个函数给他createComputedGetter

  4. createComputedGetter函数返回一个函数,会判断当前这个watcher的dirty是否为true是脏值就执行watcher.evaluate()(见2),evaluate会执行get方法赋值value,并将dirty置为false,这样下次取值就跳过了,在watcher中的update中判断lazy有的话讲dirty置为true,依赖的属性改变后下次就会执行(dep中存起来了),最后输出watcher.vlaue

  5. 第5步只是输出了value,但是在页面并不会渲染,需要在返回value之前做一步操作,如果watcher栈中有值的话,就执行渲染,把当前渲染watcher添加到dep中,这样属性变化也会执行更新渲染

if (Dep.target) { // 计算属性出栈后 还要渲染watcher, 我应该让计算属性watcher里面的属性 也去收集上一层watcher
            watcher.depend();
        }

Watch

watch原理也是一样,就是创建一个watcher,存在坚挺属性的dep上,当属性改变时,执行传入的函数就可以了 init.js

mport Watcher from './observe/watcher'

export function initState(vm) {
    const opts = vm.$options; // 获取所有的选项
    if (opts.data) {
        initData(vm);
    }
    if (opts.computed) {
        initComputed(vm);
    }
    if (opts.watch) {
        initWatch(vm);
    }
}
function initWatch(vm){
    let watch = vm.$options.watch;
    for(let key in watch){
        const handler = watch[key]; // 字符串 数组 函数
        if(Array.isArray(handler)){
            for(let i = 0; i < handler.length;i++){
                createWatcher(vm,key,handler[i]);
            }
        }else{
            createWatcher(vm,key,handler);
        }

    }
    
}

function createWatcher(vm,key,handler){
   // 字符串  函数
    if(typeof handler === 'string'){
        handler = vm[handler];
    }
    return vm.$watch(key,handler)
}

index.js

Vue.prototype.$watch = function (exprOrFn, cb) {
    // firstname
    // ()=>vm.firstname

    // firstname的值变化了 直接执行cb函数即可
    new Watcher(this,exprOrFn,{user:true},cb)
}

watcher

class Watcher { // 不同组件有不同的watcher   目前只有一个 渲染根实例的
    constructor(vm, exprOrFn, options,cb) {
        this.id = id++;
        this.renderWatcher = options; // 是一个渲染watcher

        if(typeof exprOrFn === 'string'){
            this.getter = function(){
                return vm[exprOrFn]
            }
        }else{
            this.getter = exprOrFn; // getter意味着调用这个函数可以发生取值操作
        }

      
        this.deps = [];  // 后续我们实现计算属性,和一些清理工作需要用到
        this.depsId = new Set();
        this.lazy = options.lazy;
        this.cb = cb;
        this.dirty = this.lazy; // 缓存值
        this.vm = vm;
        this.user = options.user; // 标识是否是用户自己的watcher

        this.value = this.lazy ? undefined :  this.get();
       
    }
    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;
    }
    get() {
        pushTarget(this)// 静态属性就是只有一份
        let value = this.getter.call(this.vm); // 会去vm上取值  vm._update(vm._render) 取name 和age
        popTarget() // 渲染完毕后就清空
        return value;
    }
    depend(){ // watcher的depend 就是让watcher中dep去depend
        let i =  this.deps.length;
        while(i--){
            // dep.depend()
            this.deps[i].depend(); // 让计算属性watcher 也收集渲染watcher
        }
    }
    update() {
        if(this.lazy){
            // 如果是计算属性  依赖的值变化了 就标识计算属性是脏值了
            this.dirty = true;
        }else{
            queueWatcher(this); // 把当前的watcher 暂存起来
            // this.get(); // 重新渲染
        }
    }
    run() {
        let oldValue = this.value;
        let newValue = this.get();  // 渲染的时候用的是最新的vm来渲染的
        if(this.user){
            this.cb.call(this.vm,newValue,oldValue);
        }
    }
}

watch流程分析

  1. 首先和computed一样之心初始化,遍历watch,对应的值可能是一个函数(常见),可能是一个函数数组,value如果是字符串就执行循环执行createWatcher这个方法,
  2. createWatcher判断是不是字符串,是的话从vm上取到这个函数,将key和回调函数传给$watch
  3. $watch执行new Watcher(this,exprOrFn,{user:true},cb),user为watch标识
  4. watch这时的fn可能为一个字符串,
if(typeof exprOrFn === 'string'){
            this.getter = function(){
                return vm[exprOrFn]
            }
        }e

讲cb存起来,this.cb = cd,然后执行this.get,这样就掉用了this.key,将当前的watch存在dep上了,在watcher的run函数中,判断

run() {
        let oldValue = this.value;
        let newValue = this.get();  // 渲染的时候用的是最新的vm来渲染的
        if(this.user){
            this.cb.call(this.vm,newValue,oldValue);
        }
    }

这样当监听的属性发生变化,就会执行run,就会触发cb。