侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

详解Vue 如何监听Array的变化

前端  /  管理员 发布于 5年前   393

回忆

在上一篇Vue响应式原理-理解Observer、Dep、Watcher简单讲解了Observer、Dep、Watcher三者的关系。

在Observer的伪代码中我们模拟了如下代码:

class Observer { constructor() {  // 响应式绑定数据通过方法  observe(this.data); }}export function observe (data) { const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) {  // 将data中我们定义的每个属性进行响应式绑定  defineReactive(obj, keys[i]); }}export function defineReactive () { // ...省略 Object.defineProperty get-set}

今天我们就进一步了解Observer里还做了什么事。

Array的变化如何监听?

data 中的数据如果是一个数组怎么办?我们发现Object.defineProperty对数组进行响应式化是有缺陷的。

虽然我们可以监听到索引的改变。

function defineReactive (obj, key, val) {  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: () => {      console.log('我被读了,我要不要做点什么好?');      return val;    },    set: newVal => {      if (val === newVal) {        return;      }      val = newVal;      console.log("数据被改变了,我要渲染到页面上去!");    }  })}let data = [1];// 对数组key进行监听defineReactive(data, 0, 1);console.log(data[0]); // 我被读了,我要不要做点什么好?data[0] = 2; // 数据被改变了,我要渲染到页面上去!

但是defineProperty不能检测到数组长度的变化,准确的说是通过改变length而增加的长度不能监测到。这种情况无法触发任何改变。

data.length = 0; // 控制台没有任何输出

而且监听数组所有索引的的代价也比较高,综合一些其他因素,Vue用了另一个方案来处理。

首先我们的observe需要改造一下,单独加一个数组的处理。

// 将data中我们定义的每个属性进行响应式绑定export function observe (data) {  const keys = Object.keys(data);  for (let i = 0; i < keys.length; i++) {    // 如果是数组    if (Array.isArray(keys[i])) {      observeArray(keys[i]);    } else {      // 如果是对象      defineReactive(obj, keys[i]);    }  }}// 数组的处理export function observeArray () {  // ...省略}

那接下来我们就应该考虑下Array变化如何监听?

Vue 中对这个数组问题的解决方案非常的简单粗暴,就是对能够改变数组的方法做了一些手脚。

我们知道,改变数组的方法有很多,举个例子比如说push方法吧。push存在Array.prototype上的,如果我们能
能拦截到原型上的push方法,是不是就可以做一些事情呢?

Object.defineProperty

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。存取描述符是由getter-setter函数对描述的属性,也就是我们用来给对象做响应式绑定的。Object.defineProperty-MDN

虽然我们无法使用Object.defineProperty将数组进行响应式的处理,也就是getter-setter,但是还有其他的功能可以供我们使用。就是数据描述符,数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

writable

当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。

因此我们只要把原型上的方法,进行value的重新赋值。

如下代码,在重新赋值的过程中,我们可以获取到方法名和所有参数。

function def (obj, key) {  Object.defineProperty(obj, key, {    writable: true,    enumerable: true,    configurable: true,    value: function(...args) {      console.log('key', key);      console.log('args', args);     }  });}// 重写的数组方法let obj = {  push() {}}// 数组方法的绑定def(obj, 'push');obj.push([1, 2], 7, 'hello!');// 控制台输出 key push// 控制台输出 args [Array(2), 7, "hello!"]

通过如上代码我们就可以知道,用户使用了数组上原型的方法以及参数我们都可以拦截到,这个拦截的过程就可以做一些变化的通知。

Vue监听Array三步曲

接下来,就看看Vue是如何实现的吧~

第一步:先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。

第二步:对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。

第三步:把需要被拦截的 Array 类型的数据原型指向改造后原型。

我们将代码进行下改造,拦截的过程中还是要将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变,然后我们再去做视图的更新等操作。

const arrayProto = Array.prototype // 获取Array的原型function def (obj, key) {  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    value: function(...args) {      console.log(key); // 控制台输出 push      console.log(args); // 控制台输出 [Array(2), 7, "hello!"]            // 获取原生的方法      let original = arrayProto[key];      // 将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变      const result = original.apply(this, args);      // do something 比如通知Vue视图进行更新      console.log('我的数据被改变了,视图该更新啦');      this.text = 'hello Vue';      return result;    }  });}// 新的原型let obj = {  push() {}}// 重写赋值def(obj, 'push');let arr = [0];// 原型的指向重写arr.__proto__ = obj;// 执行pusharr.push([1, 2], 7, 'hello!');console.log(arr);

被改变后的arr。

Vue源码解析

array.js

Vue在array.js中重写了methodsToPatch中七个方法,并将重写后的原型暴露出去。

// Object.defineProperty的封装import { def } from '../util/index'// 获得原型上的方法const arrayProto = Array.prototype// Vue拦截的方法const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];// 将上面的方法重写methodsToPatch.forEach(function (method) {  def(arrayMethods, method, function mutator (...args) {    console.log('method', method); // 获取方法    console.log('args', args); // 获取参数   // ...功能如上述,监听到某个方法执行后,做一些对应的操作    // 1、将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变    // 2、视图更新等  })})export const arrayMethods = Object.create(arrayProto);

observer

在进行数据observer绑定的时候,我们先判断是否hasProto,如果存在__proto__,就直接将value 的 __proto__指向重写过后的原型。如果不能使用 __proto__,貌似有些浏览器厂商没有实现。那就直接循环 arrayMethods把它身上的这些方法直接装到 value 身上好了。毕竟调用某个方法是先去自身查找,当自身找不到这关方法的时候,才去原型上查找。

// 判断是否有__proto__,因为部分浏览器是没有__proto__const hasProto = '__proto__' in {}// 重写后的原型import { arrayMethods } from './array'// 方法名const arrayKeys = Object.getOwnPropertyNames(arrayMethods);// 数组的处理export function observeArray (value) {  // 如果有__proto__,直接覆盖          if (hasProto) {    protoAugment(value, arrayMethods);  } else {    // 没有__proto__就把方法加到属性自身上    copyAugment(value, arrayMethods, )  }}// 原型的赋值function protoAugment (target, src) {  target.__proto__ = src;}// 复制function copyAugment (target, src, keys) {  for (let i = 0, l = keys.length; i < l; i++) {    const key = keys[i]    def(target, key, src[key]);  }}

通过上面的代码我们发现,没有直接修改 Array.prototype,而是直接把 arrayMenthods 赋值给 value 的 __proto__ 。因为这样不会污染全局的Array, arrayMenthods 只对 data中的Array 生效。

总结

因为监听的数组带来的代价和一些问题,Vue使用了重写原型的方案代替。拦截了数组的一些方法,在这个过程中再去做通知变化等操作。

本文的一些代码均是Vue源码简化后的,为了方便大家理解。思想理解了,源码就容易看懂了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

您可能感兴趣的文章:

  • vue App.vue中的公共组件改变值触发其他组件或.vue页面监听
  • 解决Vue.js父组件$on无法监听子组件$emit触发事件的问题
  • 使用vue.js在页面内组件监听scroll事件的方法
  • vue-cli监听组件加载完成的方法
  • vue.js项目 el-input 组件 监听回车键实现搜索功能示例
  • vue使用$emit时,父组件无法监听到子组件的事件实例
  • vue2.x 父组件监听子组件事件并传回信息的方法
  • 详解vuex 中的 state 在组件中如何监听
  • vuejs2.0实现分页组件使用$emit进行事件监听数据传递的方法


  • 上一条:
    js常见遍历操作小结
    下一条:
    JavaScript中将值转换为字符串的五种方法总结
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 使用 Alpine.js 排序插件对元素进行排序(0个评论)
    • 在js中使用jszip + file-saver实现批量下载OSS文件功能示例(0个评论)
    • 在vue中实现父页面按钮显示子组件中的el-dialog效果(0个评论)
    • 使用mock-server实现模拟接口对接流程步骤(0个评论)
    • vue项目打包程序实现把项目打包成一个exe可执行程序(0个评论)
    • 近期文章
    • 智能合约Solidity学习CryptoZombie第三课:组建僵尸军队(高级Solidity理论)(0个评论)
    • 智能合约Solidity学习CryptoZombie第二课:让你的僵尸猎食(0个评论)
    • 智能合约Solidity学习CryptoZombie第一课:生成一只你的僵尸(0个评论)
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2017-06
    • 2017-07
    • 2017-08
    • 2017-09
    • 2017-10
    • 2017-11
    • 2018-03
    • 2018-04
    • 2018-05
    • 2018-06
    • 2018-09
    • 2018-11
    • 2018-12
    • 2019-02
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2021-04
    • 2021-05
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-10
    • 2021-11
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-06
    • 2023-07
    • 2023-09
    • 2023-10
    • 2023-11
    • 2023-12
    • 2024-01
    • 2024-02
    • 2024-03
    • 2024-04
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客