Vue响应式原理-如何监听Array的变化?
2019-07-06 07:06
1381 查看
回忆
在上一篇Vue响应式原理-理解Observer、Dep、Watcher简单讲解了
Observer、
Dep、
Watcher三者的关系。
在
Observer的伪代码中我们模拟了如下代码:
[code]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对数组进行响应式化是有缺陷的。
虽然我们可以监听到索引的改变。
[code]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而增加的长度不能监测到。这种情况无法触发任何改变。
[code]data.length = 0; // 控制台没有任何输出 复制代码
而且监听数组所有索引的的代价也比较高,综合一些其他因素,Vue用了另一个方案来处理。
首先我们的
observe需要改造一下,单独加一个数组的处理。
[code]// 将data中我们定义的每个属性进行响应式绑定 export function observe (data) { const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { // 如果是数组 if (Array.isArray(keys[i])) { observeArray(obj, 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的重新赋值。
如下代码,在重新赋值的过程中,我们可以获取到方法名和所有参数。
[code]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类型的数据原型指向改造后原型。
我们将代码进行下改造,拦截的过程中还是要将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变,然后我们再去做视图的更新等操作。
[code]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; // 执行push arr.push([1, 2], 7, 'hello!'); console.log(arr); 复制代码
被改变后的
arr。
Vue源码解析
array.js
Vue在
array.js中重写了
methodsToPatch中七个方法,并将重写后的原型暴露出去。
[code]// 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身上好了。毕竟调用某个方法是先去自身查找,当自身找不到这关方法的时候,才去原型上查找。
[code]// 判断是否有__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项目如何监听窗口变化,达到页面自适应?
- vue动态数据绑定1--如何监听一个对象的变化
- 详解Vue监听数据变化原理
- vue项目如何监听窗口变化,达到页面自适应?
- vue、如何在组件挂载完成之后在监听data中对象或属性的变化
- vue.js 中如何监听watch:{}对象中的某个属性的变化,做相应的操作
- 关于如何监听div大小的变化,jquery扩展函数
- vue监听路由参数变化更新页面
- vue 监听路由变化 axios渲染
- 用Vue.js实现监听属性的变化
- Android 如何监听网络变化
- vue初识及数据驱动/ 双向数据绑定的原理 / 深入响应式的原理
- 对于单页应用中如何监听 URL 变化的思考
- Vue响应式原理详解
- vue自定义键盘信息、监听数据变化的方法示例【基于vm.$watch】
- vue监听页面大小变化重新刷新布局
- Vue 监听路由变化
- vue脚手架中webpack的打包原理,vue脚手架的webpack是如何打包的?
- Vue如何实现响应式系统
- 浅谈 Vue v-model指令的实现原理 - 如何利用v-model设计自定义的表单组件