Vue源码解析之数组变异

力有不逮的对象

众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式。当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变。

这是什么原因?

原因在于: Vue 的响应式系统是基于Object.defineProperty这个方法的,该方法可以监听对象中某个元素的获取或修改,经过了该方法处理的数据,我们称其为响应式数据。但是,该方法有一个很大的缺点,新增属性或者删除属性不会触发监听,举个栗子:

var vm = new Vue({data () {return {obj: {a: 1}}}
})
// `vm.obj.a` 现在是响应式的vm.obj.b = 2
// `vm.obj.b` 不是响应式的
复制代码

原因在于,在 Vue 初始化的时候, Vue 内部会对 data 方法的返回值进行深度响应式处理,使其变为响应式数据,所以, vm.obj.a 是响应式的。但是,之后设置的 vm.obj.b 并没有经过 Vue 初始化时响应式的洗礼,所以,理所应当的不是响应式。

那么,vm.obj.b可以变成响应式吗?当然可以,通过 vm.$set 方法就可以完美地实现要求,在此不再赘述相关原理了,之后应该会写一篇文章讲述 vm.$set 背后的原理。

更凄惨的数组

上面说了这么多,还没有提到本篇文章的主角——数组,现在该主角出场了。

比起对象,数组的境遇更加凄惨一些,看看官方文档:

由于 JavaScript 的限制, Vue 不能检测以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

有可能官方文档不是很清晰,那我们继续举个栗子:

var vm = new Vue({data () {return {items: ['a', 'b', 'c']}}
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的
复制代码

也就是说,数组连自身元素的修改也无法监听,原因在于, Vuedata 方法返回的对象中的元素进行响应式处理时,如果元素是数组时,仅仅对数组本身进行响应式化,而不对数组内部元素进行响应式化。

这也就导致如官方文档所写的后果,无法直接修改数组内部元素来触发响应式。

那么,有没有破解方法呢?

当然有,官方规定了 7 个数组方法,通过这 7 个数组方法,可以很开心地触发数组的响应式,这 7 个数组方法分别是:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

可以发现,这 7 个数组方法貌似就是原生的那些数组方法,为什么这 7 个数组方法可以触发应式,触发视图更新呢?

你是不是心里想着:数组方法了不起呀,数组方法就可以为所欲为啊?

骚瑞啊,这 7 个数组方法是真的可以为所欲为的。

因为,它们是变异后的数组方法。

数组变异思路

什么是变异数组方法?

变异数组方法即保持数组方法原有功能不变的前提下对其进行功能拓展,在 Vue 中这个所谓的功能拓展就是添加响应式功能。

将普通的数组变为变异数组的方法分为两步:

  1. 功能拓展
  2. 数组劫持

功能拓展

先来个思考题:

有这样一个需求,要求在不改变原有函数功能以及调用方式的情况下,使得每次调用该函数都能在控制台中打印出'HelloWorld'

其实思路很简单,分为三步:

  1. 使用新的变量缓存原函数
  2. 重新定义原函数
  3. 在新定义的函数中调用原函数

看看具体的代码实现:

function A () {console.log('调用了函数A')
}const nativeA = A
A = function () {console.log('HelloWorld')nativeA()
}
复制代码

可以看到,通过这种方式,我们就保证了在不改变 A 函数行为的前提下对其进行了功能拓展。

接下来,我们使用这种方法对数组原本方法进行功能拓展:

// 变异方法名称
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'
]const arrayProto = Array.prototype
// 继承原有数组的方法
const arrayMethods = Object.create(arrayProto)mutationMethods.forEach(method => {// 缓存原生数组方法const original = arrayProto[method]arrayMethods[method] = function (...args) {const result = original.apply(this, args)console.log('执行响应式功能')return result}
})
复制代码

从代码中可以看出来,我们调用 arrayMethods 这个对象中的方法有两种情况:

  1. 调用功能拓展方法:直接调用 arrayMethods 中的方法
  2. 调用原生方法:这种情况下,通过原型链查找定义在数组原型中的原生方法

通过上述方法,我们实现了对数组原生方法进行功能的拓展,但是,有一个巨大的问题摆在面前:我们该如何让数组实例调用功能拓展后数组方法呢?

解决这一问题的方法就是:数组劫持。

数组劫持

数组劫持,顾名思义就是将原本数组实例要继承的方法替换成我们功能拓展后的方法。

想一想,我们在前面实现了一个功能拓展后的数组 arrayMethods ,这个自定义的数组继承自数组对象,我们只需要将其和普通数组实例连接起来,让普通数组继承于它即可。

而想实现上述操作,就是通过原型链。

实现方法如下代码所示:

let arr = []
// 通过隐式原型继承arrayMethods
arr.__proto__ = arrayMethods// 执行变异后方法
arr.push(1)
复制代码

通过功能拓展和数组劫持,我们终于实现了变异数组,接下来让我们看看 Vue 源码是如何实现变异数组的。

源码解析

我们来到 src/core/observer/index.js 中在 Observer 类中的 constructor 函数:

constructor (value: any) {this.value = valuethis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)// 检测是否是数组if (Array.isArray(value)) {// 能力检测const augment = hasProto? protoAugment: copyAugment// 通过能力检测的结果选择不同方式进行数组劫持augment(value, arrayMethods, arrayKeys)// 对数组的响应式处理this.observeArray(value)} else {this.walk(value)}
}
复制代码

Observer 这个类是 Vue 响应式系统的核心组成部分,在初始化阶段最主要的功能是将目标对象进行响应式化。在这里,我们主要关注其对数组的处理。

其对数组的处理主要是以下代码

// 能力检测
const augment = hasProto
? protoAugment
: copyAugment
// 通过能力检测的结果选择不同方式进行数组劫持
augment(value, arrayMethods, arrayKeys)
// 对数组的响应式处理,很本文关系不大,略过
this.observeArray(value)
复制代码

首先定义了 augment 常量,这个常量的值由 hasProto 决定。

我们来看看 hasProto

export const hasProto = '__proto__' in {}
复制代码

可以发现, hasProto 其实就是一个布尔值常量,用来表示浏览器是否支持直接使用 __proto__ (隐式原型) 。

所以,第一段代码很好理解:根据根据能力检测结果选择不同的数组劫持方法,如果浏览器支持隐式原型,则调用 protoAugment 函数作为数组劫持的方法,反之则使用 copyAugment

不同的数组劫持方法

现在我们来看看 protoAugment 以及 copyAugment

function protoAugment (target, src: Object, keys: any) {/* eslint-disable no-proto */target.__proto__ = src/* eslint-enable no-proto */
}
复制代码

可以看到, protoAugment 函数极其简洁,和在数组变异思路中所说的方法一致:将数组实例直接通过隐式原型与变异数组连接起来,通过这种方式继承变异数组中的方法。

接下来我们再看看 copyAugment

function copyAugment (target: Object, src: Object, keys: Array<string>) {for (let i = 0, l = keys.length; i < l; i++) {const key = keys[i]// Object.defineProperty的封装def(target, key, src[key])}
}
复制代码

由于在这种情况下,浏览器不支持直接使用隐式原型,所以数组劫持方法要麻烦很多。我们知道该函数接收的第一个参数是数组实例,第二个参数是变异数组,那么第三个参数是什么?

// 获取变异数组中所有自身属性的属性名
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
复制代码

arrayKeys 在该文件的开头就定义了,即变异数组中的所有自身属性的属性名,是一个数组。

回头再看 copyAugment 函数就很清晰了,将所有变异数组中的方法,直接定义在数组实例本身,相当于变相的实现了数组的劫持。

实现了数组劫持后,我们再来看看 Vue 中是怎样实现数组的功能拓展的。

功能拓展

数组功能拓展的代码位于 src/core/observer/array.js ,代码如下:

import { def } from '../util/index'// 缓存数组原型
const arrayProto = Array.prototype
// 实现 arrayMethods.__proto__ === 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 insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}if (inserted) ob.observeArray(inserted)// notify changeob.dep.notify()// 返回原生数组方法的执行结果return result})
})
复制代码

可以发现,源码在实现的方式上,和我在数组变异思路中采用的方法一致,只不过在其中添加了响应式的处理。

总结

Vue 的变异数组从本质上是来说是一种装饰器模式,通过学习它的原理,我们在实际工作中可以轻松处理这类保持原有功能不变的前提下对其进行功能拓展的需求。

转载于:https://juejin.im/post/5c05465c518825158c53568e

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/281420.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux守护进程的编写

linux监控一个进程进行 代码如下: #!/bin/shcd /home/autoprocess/ autopgrep -f autoProcessNew.php | wc -l if [ "$auto" 0 ] then nohup php autoProcessNew.php & fi 监视autoProcessNew.php,使他一直监视转载于:https://www.cnblogs.com/matengfei123/p/…

微软2014编程之美初赛第一场——题目3 : 活动中心

【来源】 题目3 : 活动中心 【分析】 本题採用的是三分法。 输入的一组点中找出左右边界。作为起始边界。 while(右边界-左边界<精度){将左右边界构成的线段均匀分成3段&#xff0c;推断切割点的距离关系&#xff0c;抹去距离大的一段。更新左右边界。 } 输出左(右)边界 【…

windows10计算机里输入法,win10电脑上输入法不见了怎么办

好的输入法可以加快我们的工作效率&#xff0c;当电脑上输入法不见时&#xff0c;你会调出来吗?下面小编告诉你win10电脑上输入法不见时弄出来的一些诀窍吧。win10电脑上输入法不见了的解决方法win10电脑上输入法不见了的解决方法&#xff1a;Win10系统输入法图标不见了的找回…

Java并发(二十一):线程池实现原理

一、总览 线程池类ThreadPoolExecutor的相关类需要先了解&#xff1a; &#xff08;图片来自&#xff1a;https://javadoop.com/post/java-thread-pool#%E6%80%BB%E8%A7%88&#xff09; Executor&#xff1a;位于最顶层&#xff0c;只有一个 execute(Runnable runnable) 方法&a…

进程池

转自&#xff1a;https://www.cnblogs.com/kaituorensheng/p/4465768.html 在利用Python进行系统管理的时候&#xff0c;特别是同时操作多个文件目录&#xff0c;或者远程控制多台主机&#xff0c;并行操作可以节约大量的时间。当被操作对象数目不大时&#xff0c;可以直接利用…

gulp版本号管理插件注意事项

2019独角兽企业重金招聘Python工程师标准>>> 打开node_modules\gulp-rev\index.js 第144行 manifest[originalFile] revisionedFile; 更新为: manifest[originalFile] originalFile ?v file.revHash; 打开node_modules\rev-path\index.js 第10行 return filena…

bigfile.to服务器位置,Cloudera Manager 迁移服务器

Cloudera Manager还是比较耗资源的&#xff0c;想把Cloudera Manager&#xff0c;移动到比较好的机器上。在这篇文章中&#xff0c;Cloudera Manager安装在bigserver1上面&#xff0c;bigserver1是奔腾双核的CPU。1&#xff0c;Cloudera Manager占资源比较多cloudera manager占…

vue定时ajax获取数据,vue 中使用 AJAX获取数据的方法

在VUE开发时&#xff0c;数据可以使用jquery和vue-resource来获取数据。在获取数据时&#xff0c;一定需要给一个数据初始值。看下例&#xff1a;new Vue({el:#app,data:{data:""},created:function(){var url"json.jsp";var _selfthis;$.get(url,function…

转:shell awk

简单使用&#xff1a; awk &#xff1a;对于文件中一行行的独处来执行操作 。 awk -F &#xff1a;{print $1,$4} :使用‘&#xff1a;’来分割这一行&#xff0c;把这一行的第一第四个域打印出来 。 详细介绍&#xff1a; AWK命令介绍 awk语言的最基本功能是在文件或字符串中基…

Mac使用crontab来实现定时任务

crontab 定时执行 配置文件都在/etc/crontab下&#xff0c;如果没有就创建 语法&#xff1a; crontab [-e [UserName]|-l [UserName]|-r [UserName]|-v [UserName]|File ] 说明&#xff1a; crontab 是用来让使用者在固定时间或固定间隔执行程序之用&#xff0c;换句话说&#…

前端技术周刊 2018-12-03:DOM

前端快爆 Chrome 71 开始将试用 SXG 功能&#xff0c;它是由 IETF 提出&#xff0c;Web Package 协议规范下的 Signed HTTP Exchanges 功能的缩写。该技术使得一个第三方服务器可以直接向用户提供可靠资源&#xff0c;且不用与原站共享 HTTPS 证书密钥。?点评&#xff1a;一项…

公司新来了一位阿里P9,在全员大会上讲荤段子!还是上个世纪的老段子,太烂了!...

阿里P9在坊间的名声一向不好&#xff0c;这几年在业界出了不少令人无语的新闻&#xff0c;今天又来了一个&#xff1a;公司新来了一位阿里P9伪高管&#xff0c;全员大会上来先讲了一个荤段子&#xff0c;这个破段子还是上个世纪的&#xff0c;太烂了&#xff01;关于这个段子&a…

【转】博客美化(1)基本后台设置与样式设置

阅读目录 1.博客园后台设置2.自定义样式的设置博客园美化相关文章目录&#xff1a;博客园博客美化相关文章目录 一直都拜膜那些博客园的皮肤设计高手&#xff0c;由于本人对前端研究甚少&#xff0c;所以js,css这种东西只能看得懂最基本的&#xff0c;会简单改改。然后一直对自…

Airdoc创始人:工智能可以在医疗领域多个环节发挥作用 但有局限性

7月1日&#xff0c;在由武汉国家生物产业基地建设管理办公室主办、火石创造承办、光谷健康智慧园协办的医疗大数据与医学人工智能高峰论坛上&#xff0c;Airdoc创始人兼董事长张大磊做了题为《AI在医疗领域中应用的问题与局限》的演讲。 Airdoc是医疗领域人工智能领军企业&…

我的世界服务器抽奖系统怎么弄,我的世界自动识别货币抽奖机如何制作

我的世界是一款很经典的沙盒类游戏&#xff0c;在游戏中红石和命令方块是这部作品的核心&#xff0c;可以制作很多装备和道具&#xff0c;下面给大家分享下我的世界自动识别货币抽奖机如何制作&#xff0c;希望对大家有所帮助。自动识别货币抽奖机制作方法废话不多说,(貌似一句…

Java并发编程中volatile实现过程详细解析

2019独角兽企业重金招聘Python工程师标准>>> 首先并发编程有三大特性&#xff1a; 可见性&#xff0c;有序性&#xff0c;原子性。volatile关键字实现了前面两个特性。那么它是如何实现这两个特性的呢&#xff1f; 首先是可见性。可见性主要是让缓存&#xff0c;直接…

《ASP.NET Core 6框架揭秘》实例演示[32]:错误页面的N种呈现方式

由于ASP.NET是一个同时处理多个请求的Web应用框架&#xff0c;所以在处理某个请求过程中出现异常并不会导致整个应用的中止。出于安全方面的考量&#xff0c;为了避免敏感信息外泄&#xff0c;客户端在默认情况下并不会得到详细的出错信息&#xff0c;这无疑会在开发过程中增加…

SpringMVC接受JSON参数详解及常见错误总结我改

SpringMVC接受JSON参数详解及常见错误总结 最近一段时间不想使用Session了&#xff0c;想感受一下Token这样比较安全&#xff0c;稳健的方式&#xff0c;顺便写一个统一的接口给浏览器还有APP。所以把一个练手项目的前台全部改成Ajax了&#xff0c;跳转再使用SpringMVC控制转发…

软件定义存储的定制化怎么走?

引言 当前&#xff0c;软件定义存储成为业内超高速增长的典型。有研究人员称&#xff0c;从2014年到2019年&#xff0c;软件定义存储市场将从14亿美元增长到62亿美元以上&#xff0c;年复合增长率将达35%。软件定义存储所带来的优势显而易见&#xff0c;但是对于企业来说&#…

Golang并发模型:合理退出并发协程

goroutine作为Golang并发的核心&#xff0c;我们不仅要关注它们的创建和管理&#xff0c;当然还要关注如何合理的退出这些协程&#xff0c;不&#xff08;合理&#xff09;退出不然可能会造成阻塞、panic、程序行为异常、数据结果不正确等问题。这篇文章介绍&#xff0c;如何合…