第 123 题:Vue 是如何对数组方法进行变异的?例如 push、pop、splice 等方法

发布于 2022-08-18 12:16:16 字数 3615 浏览 226 评论 9

Vue为了增加列表渲染的功能,增加了一组观察数组的方法,而且可以显示一个数组的过滤或排序的副本。

变异方法

Vue 包含一组观察数组的变异方法,它们将会触发视图更新,包含以下方法:

push() 接收任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度
pop() 从数组末尾移除最后一项,减少数组的length值,然后返回移除的项
shift() 移除数组中的第一个项并返回该项,同时数组的长度减1
unshift() 在数组前端添加任意个项并返回新数组长度
splice() 删除原数组的一部分成员,并可以在被删除的位置添加入新的数组成员
sort() 调用每个数组项的toString()方法,然后比较得到的字符串排序,返回经过排序之后的数组
reverse() 用于反转数组的顺序,返回经过排序之后的数组
<div id="app">
  <div>
    <button @click='push'>push</button>
    <button @click='pop'>pop</button>
    <button @click='shift'>shift</button>
    <button @click='unshift'>unshift</button>
    <button @click='splice'>splice</button>
    <button @click='sort'>sort</button>
    <button @click='reverse'>reverse</button>
  </div>
  <ul>
    <li v-for="item in items" >
      {{ item.message }}
    </li>
  </ul>  
</div>
<script>
var app = new Vue({
  el: '#app',
  data: {
    items: [
      {message: 'Foo' },
      {message: 'Bar' },
      {message: 'Baz' }
    ],
    addValue:{message:'zyb'}
  },
  methods:{
    push(){
      this.items.push(this.addValue)
    },
    pop(){
      this.items.pop()
    },
    shift(){
      this.items.shift()
    },
    unshift(){
      this.items.unshift(this.addValue)
    },
    splice(){
      this.items.splice(0,1)
    },
    sort(){
     this.items.sort()
    },
    reverse(){
      this.items.reverse()
    },
  }
})
</script>

非变异方法

变异方法(mutation method),顾名思义,会改变被这些方法调用的原始数组。相比之下,也有非变异(non-mutating method)方法,例如: filter(), concat(), slice() 。这些不会改变原始数组,但总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组。

concat() 先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。
slice() 基于当前数组中一个或多个项创建一个新数组,接受一个或两个参数,即要返回项的起始和结束位置,最后返回新数组。
map() 对数组的每一项运行给定函数,返回每次函数调用的结果组成的数组、
filter() 对数组中的每一项运行给定函数,该函数会返回true的项组成的数组
<div id="app">
  <div>
    <button @click='concat'>concat</button>
    <button @click='slice'>slice</button>
    <button @click='map'>map</button>
    <button @click='filter'>filter</button>
  </div>
  <ul>
    <li v-for="item in items" >
      {{ item }}
    </li>
  </ul>  
</div>
<script>
var app = new Vue({
  el: '#app',
  data: {
    items: ['Foo','Bar','Baz'],
    addValue:'match'
  },
  methods:{
    concat(){
      this.items =  this.items.concat(this.addValue)
    },
    slice(){
      this.items =  this.items.slice(1)
    },
    map(){
      this.items =  this.items.map(function(item,index,arr){
        return index + item; 
      })
    },
    filter(){
      this.items =  this.items.filter(function(item,index,arr){
        return (index > 0); 
      })
    }
  }
})
</script>

以上操作并不会导致 Vue 丢弃现有 DOM 并重新渲染整个列表。Vue 实现了一些智能启发式方法来最大化 DOM 元素重用,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作

无法检测

由于JS的限制, Vue 不能检测以下变动的数组:
1、利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
2、修改数组的长度时,例如: vm.items.length = newLength

以下两种方式都可以实现和 vm.items[indexOfItem]=newValue 相同的效果, 同时也将触发状态更新

// Vue.set
Vue.set(example1.items, indexOfItem, newValue)
// Array.prototype.splice
arr.items.splice(indexOfItem, 1, newValue)

修改数组长度,同时也将触发状态更新

this.items.splice(2)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

感情旳空白 2022-05-04 13:53:34

变异的本质就在这些方法内部加上自定义的逻辑,其实就是想监听这些方法的调用。

Vue中默认的做法就是在数组实例与它的原型之间,插入了一个新的原型对象,这个原型方法实现了这些变异方法,也就拦截了真正数组原型上的方法(因为原型链的机制,找到了就不会继续往上找了)。
变异方法中增加了自定义逻辑,也调用了真正数组原型上的方法,即实现了目的,也不会对正常使用造成影响。

如果浏览器不支持___proto__这个属性,Vue则直接在数组实例上增加这些变异方法。

别想她╰ 2022-05-04 13:52:37

我之前没想通为什么vue会数组方法进行变异,能变异到哪里去,pop()还是pop()呀,看了诸位的回答明白了。vue是通过Object.defineProperty来监听数据的变化,但是这个方法并不能监听数组长度的变化等,所以要对数组进行hack,让vue检测到内部的变化

动态检测数组原型

	/*
	 * not type checking this file because flow doesn't play well with
	 * dynamically accessing methods on Array prototype
	 */

	var arrayProto = Array.prototype;
	var arrayMethods = Object.create(arrayProto);
        var methodsToPatch = [
		'push',
		'pop',
		'shift',
		'unshift',
		'splice',
		'sort',
		'reverse'
	];

	/**
	 * Intercept mutating methods and emit events
	 */
	methodsToPatch.forEach(function(method) {
		// cache original method
		var original = arrayProto[method];
		def(arrayMethods, method, function mutator() {
			var args = [],len = arguments.length;
			while (len--) args[len] = arguments[len];

			var result = original.apply(this, args);
			var ob = this.__ob__;
			var inserted;
			switch (method) {
				case 'push':
				case 'unshift':
					inserted = args;
					break
				case 'splice':
					inserted = args.slice(2);
					break
			}
			if (inserted) {
				ob.observeArray(inserted);
			}
			// notify change
			ob.dep.notify();
			return result
		});
	});

	/**
	 * Observe a list of Array items.
	 */
	Observer.prototype.observeArray = function observeArray(items) {
		for (var i = 0, l = items.length; i < l; i++) {
			observe(items[i]);
		}
	};

这是源码,还不是很理解。
大概意思就是调用数组的原型方法,如果推入了一个对象, 那么会调动observe的方法将对象改成响应式对象,然后再通知观察者进行更新

不再让梦枯萎 2022-05-04 13:47:02

为什么不能监听数组的变化?

function(obj, key) {
  let val = obj[key];
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log('in get');
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
    }
  });
}
var a = [1, 2, 3]
defineReactive(a, 0)
a[0] = 5 // in get

这有什么问题?

这里说的不能监听数组变化是指{b:[1,2,3]},这样去改变b[0] =2是无法触发依赖收集的
而你这种写法实际上是不能算属于数组的监听了,实际上是利用了js最顶层原型是对象的原理,你监听的是a下面的0属性,a下面的1属性,甚至是a下面的length属性
原来研究过一下这个,不太记得当时的结论了,但是刚才想了下,你非要说可以监听的话,这样是能做到监听的没问题。但是这样浪费的性能将指数倍增加,你无法预估一个数组有多少层有多少位,你需要对数组每一层每一位进行依赖收集和更新时的notify。

笔芯 2022-05-04 13:43:43

为什么不能监听数组的变化?

function(obj, key) {
  let val = obj[key];
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log('in get');
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
    }
  });
}
var a = [1, 2, 3]
defineReactive(a, 0)
a[0] = 5 // in get

这有什么问题?

蓝海似她心 2022-05-04 13:31:10

1. 为什么要对数组进行单独处理

在Vue现有阶段中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以我们需要对这些操作进行hack,让vue能监听到其中的变化。

2.怎么对数组进行处理

methodsToPatch.forEach(function(method) {
    // cache original method
    // 获取原方法
    var original = arrayProto[method];
    // def方法重新定义arrayMethods的method方法,然后将新的取值方法赋值
    def(arrayMethods, method, function mutator() {
      var args = [],
        len = arguments.length;
      while (len--) args[len] = arguments[len];

      var result = original.apply(this, args);
      var ob = this.__ob__;
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          // [].push(1),[].unshift(1)
          // arg = [1]
          inserted = args;
          break
        case 'splice':
          // [1,2,3].splice(0,1,1)
          // 第三个参数为插入的值
          inserted = args.slice(2);
          break
      }
      if (inserted) { ob.observeArray(inserted); }
      // 监听变化,如果不是插入操作直接循环响应
      // 如果是去除数组参数方法,触发一次notify将会重新计算
      // 如果仅仅是数字数据,任何操作只需要再次执行一次notify则可以
      // 但是如果新增的是一个对象类型,就需要重新监听
      // 为什么用角标和length属性不能监听的原因是因为无法触发obj的get方法,所以没法动态监听
      // notify change
      ob.dep.notify();
      return result
    });
  });

正如该题所问,vue对push,pop,splice等方法进行了hack,hack方式很简单,如果加入新对象,对新对象进行响应式化,至于如何响应式化请参考vue源码。
举例来说对于push和unshift会推入一个新的对象到数组里(不管从前还是从后),记录这个加入的对象,并调用Observe方法将加入的对象转换成响应式对象,对于splice方法,如果加入了新对象也是将该对象响应式化。
最后一步是向外抛出数组变化,提醒观察者进行更新。

3.存在问题

对于Object.defineProperty的缺陷导致如果直接改变数组下标是无法hack的,由于此点,vue提供了$set方法,最新的解决方案当然是利用Proxy对象进行监听,但是Proxy的缺陷在于兼容性,可能会为了性能以及便利而放弃兼容性吧,一切都要看尤大的决定了。

4.ps

不知道啥时候3.0才能出来,都等到迫不及待了。

只为一人 2022-05-04 12:15:06

const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach((method) => {
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        value: function mutaor(...args) {
            return original.apply(this, args);
        },
        enumerable: false,
        writable: true,
        configurable: true
    })
})
东走西顾 2022-05-04 12:06:49

第一次issue。。。
直接上源码

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {  
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

简单来讲,重写了数组中的那些方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化,然后手动调用notify,通知渲染watcher,执行update

疯了 2022-05-04 11:35:27


对于这些变异方法vue做了包裹,在原型上进行了拦截,调用原生的数组方法后,还会执行发布和变更的操作来触发视图的更新。

扛起拖把扫天下 2022-05-04 01:10:05

原型拦截

~没有更多了~

关于作者

幼儿园老大

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文