您现在的位置是:首页 >技术杂谈 >手写vue-computed网站首页技术杂谈

手写vue-computed

cwj&xyp 2024-06-18 18:01:02
简介手写vue-computed

一、计算属性的使用

1、引入计算属性

插值(即双大括号表达式),支持简单的表达式和过滤器可以对值做一些简单运算和文本转换
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的
在模板中放入太多的逻辑会让模板过重且难以维护

例如:

<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

过长的表达式中包含了3个操作,模板不再是简单的声明式逻辑,复杂的逻辑会使代码变得难以理解(可读性差)和维护,且无法复用
所以,对于任何复杂逻辑,都应使用计算属性

2、认识计算属性

现象:页面中三个地方使用了计算属性fullname,控制台打印run只有一次

  • 依赖的值发生变化才会重新执行用户的方法 计算属性中维护一个dirty值,默认是脏的,第一次取值会执行这个方法, 默认计算属性不会立刻执行
  • 计算属性就是一个definePropoty
  • 计算属性也是一个Watcher,默认渲染也会创建一个渲染Watcher,计算属性watcher包含在渲染watcher中
 <div id="app">{{fullname}} {{fullname}} {{fullname}}</div>
 // 默认写一个函数 是get方法
computed: {
		//fullname()
		//{return this.fistname + this.lastname;},
          fullname: {
            get() {
              console.log("run");
              return this.fistname + this.lastname;
            },
            set(newValue) {
              console.log(newVal);
            },
          },
        },

二、实现原理

如何做到渲染?渲染watcher中包含多个计算属性watcher,计算属性里又依赖了多个属性,更新引用的属性值,每个属性有自己的dep,会收集watcher,收集到的就是计算属性watcher
脏值dirty刚刚开始为true,修改计算属性依赖的属性,只会引起dirty的变化,并不会重新渲染视图
需要依赖的属性变化时重新渲染视图
让依赖的属性的dep不仅记住计算属性的watcher还要记住渲染watcher

三、实现计算属性

1、准备

1、1dep.js

在dep.js中增加一个栈结构和两个方法,由于watcher不再是一个,此时包含计算属性watcher和渲染watceher,存储方式改为栈性结构

let stack = []
// 将watcher放入stack中
export function pushTarget(watcher){
    stack.push(watcher);
    Dep.target = watcher;
}
// 渲染完之后将watcher出栈
export function popTarget(){
    stack.pop();
    Dep.target = stack[stack.length - 1];
}

1、2wathcer.js

watcher保存方法改变

get() {
    pushTarget(this);
    // Dep.target = this; // 在类上增加了一个静态属性,静态属性只有一份   this就是watcher
    this.getter(); //会去vm上取值 vm._update(vm._render)取值
    popTarget();
    // Dep.target = null; //渲染完毕后清空
  }

2、初始化computed

initState(vm)方法中初始化计算属性、watch

export function initState(vm) {
  const opts = vm.$options;
  ...
  if (opts.computed) {
    // 初始化计算属性
    initComputed(vm);
  }
}

2.1取值

function initComputed(vm) {
  const computed = vm.$options.computed;
  for (let key in computed) {
    let userDef = computed[key];
    
    defineComputed(vm, key, userDef);
  }
}

function defineComputed(target, key, userDef) {
  // 由于计算属性有两种写法 需要判断是否为方法,是方法则为get方法,否则取对象的get方法
  const getter = typeof userDef === "function" ? userDef : userDef.get;
  const setter = userDef.set || (() => {});
  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
  });
}

问题:
在页面上取值三次,会执行三次计算属性的get方法

2.2 缓存

function initComputed(vm) {
  const computed = vm.$options.computed;
  let watchers = {};
  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(wm, fn, { lazy: true });

    defineComputed(vm, key, userDef);
  }
}
function defineComputed(target, key, userDef) {
  // 由于计算属性有两种写法 需要判断是否为方法,是方法则为get方法,否则取对象的get方法
  // const getter = typeof userDef === "function" ? userDef : userDef.get;
  const setter = userDef.set || (() => {});
  Object.defineProperty(target, key, {
    get: createComputedGetter(key),
    set: setter,
  });
}

实现computed多次取值只执行一次get方法:dirty初始值为true,在取值方法中设置为false,将返回值放到watcher上,后续取值判断dirty为false,则直接取watcher上的值。

// 需要检测是否需要执行getter
function createComputedGetter(key) {
  return function () {
    const watcher = this._computedWatchers[key]; //获取到对应计算熟悉的watcher
    if (watcher.dirty) {
      //执行用户传入的函数 还需要改变dirty的值
      watcher.evaluate(); //求值后 多次取值,dirty都是false 走缓存
    }
    return watcher.value; //最后返回的是watcher上的值
  };
}
  evaluate() {
    this.value = this.get(); //获取到用户函数的返回值,并标识为脏
    this.dirty = false;
  }
  get() {
    pushTarget(this);
    // Dep.target = this; // 在类上增加了一个静态属性,静态属性只有一份   this就是watcher
    // 执行getter时的this绑定为vm
    let value = this.getter.call(this.vm); //会去vm上取值 vm._update(vm._render)取值
    popTarget();
    // Dep.target = null; //渲染完毕后清空
    // 拿到执行用户函数后的返回值
    return value;
  }

此时修改firstname的值,并不更新fullname的值

2.3 修改值

更新计算属性依赖的值,将dirty设置为true,取值时重更新执行取值方法

 update() {
    // 如果是计算属性 依赖的值发生变化 就标识着计算属性是脏值
    if (this.lazy) {
      this.dirty = true
    } else {
      // this.get() //重新渲染 这样会每次刷新都更新,希望实现异步更新,将所有同步的更新执行完,再进行
      queueWatcher(this); //将所有的watcher放到队列中,对watcher去重
    }

  }

这样仅仅更新了计算属性的watcher,还需要更新渲染watcher
取值后,让依赖的属性记住渲染watcher

function createComputedGetter(key) {
  return function () {
    const watcher = this._computedWatchers[key]; //获取到对应计算熟悉的watcher
    if (watcher.dirty) {
      //执行用户传入的函数 还需要改变dirty的值
      watcher.evaluate(); //求值后 多次取值,dirty都是false 走缓存
    }
    if(Dep.target){ //计算属性watcher出栈后 还有渲染watcher 应该让计算属性watcher里面的属性也去收集上层watcher
      watcher.depend()
    }
    return watcher.value; //最后返回的是watcher上的值
  };
}
 depend(){
    let i = this.deps.length;
    while(i--){
      // dep上收集watcher
      this.deps[i].depend() //让计算属性watcher也收集渲染watcher
    }
  }

计算属性不会收集依赖,只会让自己依赖的属性去收集依赖

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。