切图妞

vuePress-theme-reco 切图妞    2020 - 2021
切图妞 切图妞
前端知识梳理
  • Vue
  • 浏览器 & 网络
  • HTML & CSS
  • Web安全
  • 算法
文章分类
  • 前端小麻烦
  • 配置乐园
  • 实战不完全手册
  • 手撕源码
宝藏女孩
  • 模板仓
  • 项目简介
  • GitHub
  • Segmentfault
  • CSDN
时间轴
author-avatar

切图妞

19

Article

18

Tag

前端知识梳理
  • Vue
  • 浏览器 & 网络
  • HTML & CSS
  • Web安全
  • 算法
文章分类
  • 前端小麻烦
  • 配置乐园
  • 实战不完全手册
  • 手撕源码
宝藏女孩
  • 模板仓
  • 项目简介
  • GitHub
  • Segmentfault
  • CSDN
时间轴

vue的响应式设计

vuePress-theme-reco 切图妞    2020 - 2021

vue的响应式设计

切图妞 2020-05-10 VueVue进阶

Vue 不支持 IE8 以及更低版本浏览器;已经创建的对象实例,Vue 不允许后加根级别的响应式 property;Vue无法检测利用索引直接设置一个数组项的情况;Vue无法检测修改数组的长度的情况。所以proxy来咯!

github代码

# 一、Object. defineProperty

vue官网——深入响应式原理

MDN——Object. defineProperty

Object.defineProperty(obj, prop, descriptor)  方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  • 参数
    • obj 要定义属性的对象。
    • prop 要定义或修改的属性的名称或 Symbol 。
    • descriptor 要定义或修改的属性描述符。
  • 返回值
    • 被传递给函数的对象。

# 场景

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter 。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

![img](https://cdn.nlark.com/yuque/0/2020/png/543173/1590734745414-7c80beb2-ec1c-461d-8599-f68acc6a45c7.png#align=left&display=inline&height=375&margin=%5Bobject%20Object%5D&name=image. png&originHeight=750&originWidth=1200&size=65135&status=done&style=none&width=600)

# 缺点

  1. Vue 不支持 IE8 以及更低版本浏览器, Object.defineProperty 是 ES5 中一个无法 shim 的特性。

  2. Object.defineProperty 是只能监听对象,不能监听数组

    • 数组方法需要通过继承创建自己的原型,重写methods方法
  3. 对于已经创建的对象实例,Vue 不允许后加根级别的响应式 property,例如: vm.b = 2

    • 使用  Vue.set(object, propertyName, value)  方法向嵌套对象添加响应式 property
    • 已有对象赋值多个新 property,可以使用使用  Object.assign()
  4. Vue无法检测利用索引直接设置一个数组项的情况,例如: vm.items[indexOfItem] = newValue

    • Vue.set(vm.items, indexOfItem, newValue)
    • vm.items.splice(indexOfItem, 1, newValue)
  5. Vue无法检测修改数组的长度的情况,例如: vm.items.length = newLength

    • vm.items.splice(newLength) , 改方法只能长变成短,不能短变成长
    let obj1 = {
        name: 'ct',
        address: {
            province: 'fujian',
            city: 'fuzhou'
        },
        like: []
    }
    // 无法检测
    obj1.age = 18
    obj1.like[3] = 'sport'
    obj1.like.length--

    //替换方案
    obj1 = Object.assign({}, obj1, {
        age: 18
    })
    $set(obj1.like, 'like', 'sing')
    obj1.like.splice(0)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 手写代码

  1. 模拟渲染的render函数
  2. observer函数实时监听数据变化,数据为对象时需要递归循环内部数据
  3. defineReactive函数接受对象,key值和value值,Object.defineProperty方法通过set和get方法进行设置和获取。
  4. $set函数接受对象,key值和value值实现强制更新功能。
  5. 数组形式的数据,需要通过原型链继承原生方法,并且添加render().实例的隐式原型中添加方法。$set方法中数组通过data.splice(key, 1, value)处理数据。
  // 模拟渲染
  function render() {
      console.log('模拟渲染')
  }
  // 处理数组
  const methods = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']
  // 先要获取原来原型上的方法
  const arrayProto = Array.prototype
  // 通过继承,不改变原来的方法还能使用方法
  let proto = Object.create(arrayProto)
  // 重写方法,调用方法时进行渲染,还有记得调用原来的方法,进行回调
  methods.forEach(method => {
      proto[method] = function() {
          render()
          arrayProto[method].call(this, ...arguments)
      }
  })
  // 监听数据变化
  function observer(data) {
      if (Array.isArray(data)) {
          data.__proto__ = proto
          return
      }
      if (typeof data == 'object') {
          for (let key in data) {
              defineReactive(data, key, data[key])
          }
      }
  }

  // 获取值,改变值
  function defineReactive(data, key, value) {
      observer(value)
      Object.defineProperty(data, key, {
          get() {
              return value
          },
          set(newValue) {
              observer(newValue)
              if (newValue == value) return
              render()
              value = newValue
          }
      })
  }
  // 强制更新
  function $set(data, key, value) {
      if (Array.isArray(data)) {
          return data.splice(key, 1, value)
      }
      defineReactive(data, key, value)
  }

  // 测试
  let obj1 = {
      name: 'ct',
      address: {
          province: 'fujian',
          city: 'fuzhou'
      },
      like: []
  }
  observer(obj1)
  obj1.name = 'qietuniu'
  obj1.address.city = 'xiamen'
  $set(obj1.like, 'like', 'sing')
  obj1.like.push('dance')

  // 无法生效
  // obj1.like.length--
  // obj1.like[3] = 'sport'

  console.log('obj1', obj1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

![image. png](https://cdn.nlark.com/yuque/0/2020/png/543173/1590733054947-73156246-3683-452d-b853-fdae2be96e36.png#align=left&display=inline&height=396&margin=%5Bobject%20Object%5D&name=image. png&originHeight=792&originWidth=772&size=105972&status=done&style=none&width=386)

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。

# 扩展

# get和set

MND——get和set

getter  是一个获取某个特定属性的值的方法。  setter  是一个设定某个属性的值的方法。

get 属性的 getter 函数,如果没有 getter,则为 undefined 。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的 this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined

set 属性的 setter 函数,如果没有 setter,则为 undefined 。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined 。

# 枚举一个对象的所有属性

  • for... in 循环

该方法依次访问一个对象及其原型链中所有可枚举的属性。

  • Object. keys(o)

该方法返回对象 o 自身包含(不包括原型中)的所有可枚举属性的名称的数组。

  • Object. getOwnPropertyNames(o)

该方法返回对象 o 自身包含(不包括原型中)的所有属性(无论是否可枚举)的名称的数组。

es5之前:

function listAllProperties(o) {
    var objectToInspect;
    var result = [];

    for (objectToInspect = o; objectToInspect !== null; objectToInspect = Object.getPrototypeOf(objectToInspect)) {
        result = result.concat(Object.getOwnPropertyNames(objectToInspect));
    }

    return result;
}
1
2
3
4
5
6
7
8
9
10

# 创建新对象

  1. 对象初始化器: var myHonda = {color: "red"};

  2. 使用构造函数

  • 通过创建一个构造函数来定义对象的类型。首字母大写是非常普遍而且很恰当的惯用法。
  • 通过 new 创建对象实例。
function Car(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
}

var mycar = new Car("Eagle", "Talon TSi", 1993);
1
2
3
4
5
6
7
  1. 使用 Object.create 方法

对象也可以用 Object.create()  方法创建。该方法非常有用,因为它允许你为创建的对象选择一个原型对象,而不用定义构造函数。

// Animal properties and method encapsulation
var Animal = {
    type: "Invertebrates", // 属性默认值
    displayType: function() { // 用于显示type属性的方法
        console.log(this.type);
    }
}

// 创建一种新的动物——animal1 
var animal1 = Object.create(Animal);
animal1.displayType(); // Output:Invertebrates

// 创建一种新的动物——Fishes
var fish = Object.create(Animal);
fish.type = "Fishes";
fish.displayType(); // Output:Fishes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# Object. create

MND——Object. create

  • 参数
    • proto 新创建对象的原型对象。
    • propertiesObject 可选。如果没有指定为 undefined ,则是要添加到新创建对象的不可枚举(默认)属性(即其自身定义的属性,而不是其原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties() 的第二个参数。
  • 返回值
    • 一个新对象,带着指定的原型对象和属性。
    • 如果 propertiesObject 参数是 null 或非原始包装对象,则抛出一个 TypeError 异常。

用 Object. create实现类式继承:

// Shape - 父类(superclass)
function Shape() {
    this.x = 0;
    this.y = 0;
}

// 父类的方法
Shape.prototype.move = function(x, y) {
    this.x += x;
    this.y += y;
    console.info('Shape moved.');
};

// Rectangle - 子类(subclass)
function Rectangle() {
    Shape.call(this); // call super constructor.
}

// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log('Is rect an instance of Rectangle?',
    rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?',
    rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

如果你希望能继承到多个对象,则可以使用混入的方式。

function MyClass() {
    SuperClass.call(this);
    OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
    // do a thing
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Object. assign  会把   OtherSuperClass 原型上的函数拷贝到  MyClass 原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法

#

# 二、Proxy

proxy代理,在我们访问对象前添加了一层拦截,可以过滤很多操作,而这些过滤,由你来定义。 const p = new Proxy(target, handler) ,用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

  • 参数
    • target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
    • handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

# handle

handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap)。所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

  • handler.getPrototypeOf()
  • handler.setPrototypeOf()
  • handler.isExtensible()
  • handler.preventExtensions()
  • handler.getOwnPropertyDescriptor()
  • handler.defineProperty()
  • [handler.has()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/has) :in操作符的捕捉器。
  • handler.get() :属性读取操作的捕捉器。
  • handler.set() :属性设置操作的捕捉器。
  • handler.deleteProperty()  :delete操作符的捕捉器。
  • [handler.ownKeys()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/ownKeys) :Object.getOwnPropertyNames 和 Object.getOwnPropertySymbols 方法的捕捉器。
  • handler.apply() :函数调用操作的捕捉器。
  • handler.construct() :new操作作符的捕捉器。

# 场景

# 基础——无操作转发代理

let target = {};
let p = new Proxy(target, {});

p.a = 37; // 操作转发到目标

console.log(target.a); // 37. 操作已经被正确地转发
1
2
3
4
5
6

# 扩展构造函数

方法代理可以轻松地通过一个新构造函数来扩展一个已有的构造函数。这个例子使用了construct 和 apply 。

function extend(sup, base) {
    var descriptor = Object.getOwnPropertyDescriptor(
        base.prototype, "constructor"
    );
    base.prototype = Object.create(sup.prototype);
    var handler = {
        construct: function(target, args) {
            var obj = Object.create(base.prototype);
            this.apply(target, obj, args);
            return obj;
        },
        apply: function(target, that, args) {
            sup.apply(that, args);
            base.apply(that, args);
        }
    };
    var proxy = new Proxy(base, handler);
    descriptor.value = proxy;
    Object.defineProperty(base.prototype, "constructor", descriptor);
    return proxy;
}

var Person = function(name) {
    this.name = name
};

var Boy = extend(Person, function(name, age) {
    this.age = age;
});

Boy.prototype.sex = "M";

var Peter = new Boy("Peter", 13);
console.log(Peter.sex); // "M"
console.log(Peter.name); // "Peter"
console.log(Peter.age); // 13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 通过属性查找数组中的特定对象

通过 Proxy,我们可以灵活地“定义”属性,而不需要使用 Object.defineProperties  方法。以下例子可以用于通过单元格来查找表格中的一行。在这种情况下,target 是 table.rows 。

let products = new Proxy([{
        name: 'Firefox',
        type: 'browser'
    },
    {
        name: 'SeaMonkey',
        type: 'browser'
    },
    {
        name: 'Thunderbird',
        type: 'mailer'
    }
], {
    get: function(obj, prop) {
        // 默认行为是返回属性值, prop ?通常是一个整数
        if (prop in obj) {
            return obj[prop];
        }

        // 获取 products 的 number; 它是 products.length 的别名
        if (prop === 'number') {
            return obj.length;
        }

        let result, types = {};

        for (let product of obj) {
            if (product.name === prop) {
                result = product;
            }
            if (types[product.type]) {
                types[product.type].push(product);
            } else {
                types[product.type] = [product];
            }
        }

        // 通过 name 获取 product
        if (result) {
            return result;
        }

        // 通过 type 获取 products
        if (prop in types) {
            return types[prop];
        }

        // 获取 product type
        if (prop === 'types') {
            return Object.keys(types);
        }

        return undefined;
    }
});

console.log(products[0]); // { name: 'Firefox', type: 'browser' }
console.log(products['Firefox']); // { name: 'Firefox', type: 'browser' }
console.log(products['Chrome']); // undefined
console.log(products.browser); // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]
console.log(products.types); // ['browser', 'mailer']
console.log(products.number); // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# proxy和Object. defineProperty区别

  • Proxy 可以直接监听对象而非属性;
  • Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法, 不限于 apply、ownKeys、deleteProperty、has 等
  • Proxy 返回的是一个新对象, 我们可以只操作新的对象达到目的, 而 Object. defineProperty 只能遍历对象属性直接修改
  • Proxy兼容性问题, 而且无法用polyfill磨平

# 手写代码

let obj = {
    name: 'ct',
    address: {
        province: 'fujian',
        city: 'fuzhou'
    },
    like: []
}
// 模拟渲染
function render() {
    console.log('模拟渲染')
}

let handler = {
    get(target, key, receiver) {
        if (typeof target[key] == 'object' && typeof target[key] !== null) {
            return new Proxy(target[key], handler)
        }
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        if (key === 'length') return true
        render()
        return Reflect.set(target, key, value, receiver)
    }
}

let proxy = new Proxy(obj, handler)
proxy.address.city = 'xiamen'
proxy.like.push('sing')
proxy.arr--
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 扩展

# Reflect

MDN——Reflect

Javascript中的Reflect对象

  1. 更加有用的返回值

Object. defineProperty(obj, name, desc)执行成功会返回obj, 以及其它原因导致的错误, Reflect. defineProperty只会返回false或者true来表示对象的属性是否设置上了

try {
    Object.defineProperty(obj, name, desc);
    // property defined successfully
} catch (e) {
    // possible failure (and might accidentally catch the wrong exception)
}

if (Reflect.defineProperty(obj, name, desc)) {
    // success
} else {
    // failure
}
1
2
3
4
5
6
7
8
9
10
11
12
  1. 函数操作

name in obj ; 或者删除一个属性 :delete obj[name],  虽然这些很好用, 很简短, 很明确, 但是要使用的时候也要封装成一个类;   有了Reflect, 它帮你封装好了, Reflect. has(obj, name),  Reflect. deleteProperty(obj, name) **

  1. 更加可靠的函数式执行方式

要执行一个函数f,并给它传一组参数args, 还要绑定this的话, 要这么写:

f.apply(obj, args)
// 但是f的apply可能被重新定义成用户自己的apply了,所以还是这样写比较靠谱:
Function.prototype.apply.call(f, obj, args)
// reflect
Reflect.apply(f, obj, args)
1
2
3
4
5
  1. 可变参数形式的构造函数
var obj = new F(...args)
1

不过在ES5中, 不支持扩展符啊, 所以, 我们只能用F. apply,或者F. call的方式传不同的参数, 可惜F是一个构造函数,我们在ES5中能够这么写:

var obj = Reflect.construct(F, args)
1
  1. 控制访问器或者读取器的this
  var name = ... // get property name as a string
      obj[name] // generic property lookup
  obj[name] = value // generic property update
1
2
3

Reflect. get和Reflect. set方法允许我们做同样的事情, 而且他增加了一个额外的参数reciver, 允许我们设置对象的setter和getter的上下this:

  var name = ... // get property name as a string
      Reflect.get(obj, name, wrapper) // if obj[name] is an accessor, it gets run with `this === wrapper` 
  Reflect.set(obj, name, value, wrapper)
1
2
3

访问器中不想使用自己的方法,而是想要重定向this到wrapper:

  var obj = {
      set foo(value) {
          return this.bar();
      },
      bar: function() {
          alert(1);
      }
  };
  var wrapper = {
      bar: function() {
          console.log("wrapper");
      }
  }
  Reflect.set(obj, "foo", "value", wrapper);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 避免直接访问 proto

ES5提供了 Object. getPrototypeOf(obj),去访问对象的原型, ES6提供也提供了 Reflect.getPrototypeOf(obj)  和  Reflect. setPrototypeOf(obj, newProto), 这个是新的方法去访问和设置对象的原型