【翻译】Vue.js中的computed是如何工作的

1,651 阅读4分钟
建议英文还可以的朋友直接读原版:skyronic.com/blog/vuejs-…,原文不难。如果有朋友觉得不好理解的话读我这篇也可以,我会在文中有些晦涩的地方加上自己的备注(用行内代码块表示)方便理解。这是我个人第一次翻译文章,如果有不对的地方还望指正,谢谢!

正文:

这篇文章,我们通过实现一个简单版的和Vue中computed具有相同功能的函数来了解computed是如何工作的。

JS属性:

JavaScript有一个特性是Object.defineProperty,它能做很多事,但我在这篇文章只专注于这个方法中的一个:

var person = {};

Object.defineProperty (person, 'age', {
  get: function () {
    console.log ("Getting the age");
    return 25;
  }
});

console.log ("The age is ", person.age);

// Prints:
//
// Getting the age
// The age is 25

(Obeject.defineProperty是Object的一个方法,第一个参数是对象名称,第二个参数是要设置的属性名,第三个参数是一个对象,它可以设置这个属性是否可修改、可写等,而这篇文章主要使用的是Obeject.defineProperty的访问器属性,感兴趣的朋友可以自行google或者查看Js高及程序设计)

尽管person.age看起来像是访问了对象的一个属性,但其实在内部我们是运行了一个函数。

一个基本可响应的Vue.js

Vue.js内部构建了一个可以将普通的对象转化为可以被观察的值(响应属性),下面为大家展示一个简化版的如何添加响应属性的案例:

function defineReactive (obj, key, val) {
  Object.defineProperty (obj, key, {
    get: function () {
      return val;
    },
    set: function (newValue) {
      val = newValue;
    }
  })
};

// 创建一个对象
var person = {};

// 添加可响应的属性"age"和"country"
defineReactive (person, 'age', 25);
defineReactive (person, 'country', 'Brazil');

// 现在你可以随意使用person.age了
if (person.age < 18) {
  return 'minor';
}
else {
  return 'adult';
}

// 设置person.country的值
person.country = 'Russia';

有趣的是,25‘Brazil’还是一个闭包内部的变量,只有当赋给它们新值的时候val才会改变。person.country并不拥有'Brazil'这个值,而是getter这个函数拥有'Brazil'这个值。

声明一个计算属性

让我们创建一个定义计算属性的函数defineComputed。这个函数就跟大家平时使用computed时的一样。

defineComputed (
  person, // 计算属性就声明在这个对象上
  'status', // 计算属性的名称
  function () { // 实际返回计算属性值的函数
    console.log ("status getter called")
    if (person.age < 18) {
      return 'minor';
    }
    else {
      return 'adult';
    }
  },
  function (newValue) {
    // 当计算属性值更新时调用的函数
    console.log ("status has changed to", newValue)
  }
});

// 我们可以像使用一般的属性一样使用计算属性
console.log ("The person's status is: ", person.status);

让我们写一个简单的defineComputed函数,它支持调用计算方法,但目前不需要它支持updateCallback

function defineComputed (obj, key, computeFunc, updateCallback) {
  Object.defineProperty (obj, key, {
    get: function () {
      // 执行计算函数并且返回值
      return computeFunc ();
    },
    set: function () {
      // 什么也不做,不需要设定计算属性的值
    }
  })
}

这个函数有两个问题:

  1. 每次访问计算属性时都会执行一次计算函数computeFunc ()
  2. 它不知道什么时候更新(即当我们更新某个data中的属性,计算属性中也会更新这个data属性)

// 我希望最终函数执行后是这个效果:每当person.age更新值的时候,person.status也同步更新

person.age = 17;
// console: status 的值为 minor

person.age = 22;
// console: status 的值为 adult

增加一个依赖项

让我们增加一个全局变量Dep

var Dep = {
  target: null
};

这是一个依赖项,接着我们用一个骚操作来更新defineComputed函数:

function defineComputed (obj, key, computeFunc, updateCallback) {
  var onDependencyUpdated = function () {
    // TODO
  }
  Object.defineProperty (obj, key, {
    get: function () {
      // 将onDependencyUpdated 这个函数传给Dep.target
      Dep.target = onDependencyUpdated;
      var value = computeFunc ();
      Dep.target = null;
    },
    set: function () {
      // 什么也不做,不需要设定计算属性的值
    }
  })
}

现在让我们回到之前设置的响应属性上:

function defineReactive (obj, key, val) {
  // 所有的计算属性都依赖这个数组
  var deps = [];

  Object.defineProperty (obj, key, {
    get: function () {
      // 检查是否调用了计算属性,如果调用了,Department.target将等于一个onDependencyUpdated函数
      if (Dep.target) {
        // 把onDependencyUpdated函数push到deos中
        deps.push (target);
      }

      return val;
    },
    set: function (newValue) {
      val = newValue;

      // 通知所有的计算属性,告诉它们有个响应属性更新了
      deps.forEach ((changeFunction) => {
        changeFunction ();
      });
    }
  })
};

我们可以在计算属性定义的函数触发更新回调后更新onDependencyUpdated函数。

var onDependencyUpdated = function () {
  // 再次计算 计算属性的值
  var value = computeFunc ();
  updateCallback (value);
}

把它们整合到一起:

让我们重新访问我们的计算属性person.status:

person.age = 22;

defineComputed (
  person,
  'status',
  function () {
    if (person.age > 18) {
      return 'adult';
    }
  },
  function (newValue) {
    console.log ("status has changed to", newValue)
  }
});

console.log ("Status is ", person.status);