深入了解 Vue 响应式原理(数据拦截)
栏目: JavaScript · 发布时间: 5年前
内容简介:在上一章节我们已经粗略的分析了整个的Vue 的源码,但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。这一章节我们针对1. 深入了解 Vue 响应式原理(数据拦截)来进行分析。我们在上一章节中已经分析了,在初始化Vue实例的时候,会执行
在上一章节我们已经粗略的分析了整个的Vue 的源码,但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。
- 深入了解 Vue 响应式原理(数据拦截)
- 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所需修改
- 深入了解 Virtual DOM
- 深入了解 Vue.js 的批量异步更新策略
- 深入了解 Vue.js 内部运行机制,理解调用各个 API 背后的原理
这一章节我们针对1. 深入了解 Vue 响应式原理(数据拦截)来进行分析。
initState
我们在上一章节中已经分析了,在初始化Vue实例的时候,会执行 _init
方法, 其中会执行 initState
方法, 这个方法非常重要, 其对我们 new Vue
实例化对象时,传递经来的参数 props
, methods
, data
, computed
, watch
的处理。 其代码如下:
function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } if (opts.methods) { initMethods(vm, opts.methods); } if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */); } if (opts.computed) { initComputed(vm, opts.computed); } if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } } 复制代码
这一章节,我们只分析对 data
的处理, 也就是 initData(vm)
方法, 其代码如下(删除了异常处理的代码):
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; var keys = Object.keys(data); var props = vm.$options.props; var methods = vm.$options.methods; var i = keys.length; while (i--) { var key = keys[i]; { if (methods && hasOwn(methods, key)) { warn( ("Method \"" + key + "\" has already been defined as a data property."), vm ); } } if (props && hasOwn(props, key)) { warn( "The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.", vm ); } else if (!isReserved(key)) { proxy(vm, "_data", key); } } // observe data observe(data, true /* asRootData */); } 复制代码
从上面的代码分析,首先可以得出如下一个
总结:
- data里面的key一定不能和methods, props里面的key重名
-
proxy(vm, "_data", key);
只是将data
里面的属性重新挂载(代理)在vm
实例上,我们可以通过如下两种方式访问data
里面的数据, 如vm.visibility
或者vm._data.visibility
效果是一样的。observe(data, true /* asRootData */);
是最重要的一个方法,下面我们来分析这个方法
observe
observe
中文翻译就是 观察
, 就是将原始的 data
变成一个 可观察的对象
, 其代码如下(删除了一些逻辑判断):
function observe (value, asRootData) { ob = new Observer(value); } 复制代码
这个方法就是 new
了一个 Observer
对象, 其构造函数如下:
var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } }; 复制代码
这个方法里面有对 Array
做特殊处理,我们现在传递的对象是一个 Object
, 但是里面 todos
是一个数组,我们后面会分析数组处理的情况, 接下来调用 this.walk
方法,就是遍历对象中的每一个属性:
Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } }; 复制代码
defineReactive$$1
方法通过 Object.defineProperty
来重新封装 data
, 给每一个属性添加一个 getter
, setter
来做数据拦截
function defineReactive$$1 ( obj, key, val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); } 复制代码
defineReactive$$1
方法就是利用 Object.defineProperty
来设置 data
里面已经 存在 的属性来设置 getter
, setter
, 具体 get
和 set
在什么时候发挥效用我们先不分析。
var childOb = !shallow && observe(val);
是一个递归调 observe
来拦截所有的子属性。
在 data
中的属性 todos
是一个数组, 我们又回到 observe
方法, 其主要目的是通过 ob = new Observer(value);
来生成一个 Observer
对象:
var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } }; 复制代码
这里可以看出对 Array
有特殊的处理,下面我们我们来具体分析 protoAugment
方法
protoAugment(数组)
protoAugment(value, arrayMethods);
传了两个参数,第一个参数,就是我们的数组,第二个参数 arrayMethods
需要好好分析,是 Vue
中对 Array
的特殊处理的地方。
其源码文件在 vue\src\core\observer\array.js
下,
- 首先基于
Array.prototype
原型创建了一个新的对象arrayMethods
const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) 复制代码
- 重写了
Array
如下 7 个方法:
var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; 复制代码
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 }) }) 复制代码
总结:从上面可知, Vue
只会对上述七个方法进行监听, 如果使用Array 的其他的方法是不会触发Vue 的双向绑定的。比如说用 concat
, map
等方法都不会触发双向绑定。
this.$set
上面已经分析了 Object
, Array
的数据监听,但是上面的情况都是在初始化 Vue
实例的时候,已经知道了 data
中有哪些属性了,然后对每个属性进行数据拦截,现在有一种情况就是,如果我们有需要需要给 data
动态的添加属性,我们该怎么做呢?
Vue
单独开放出了一个接口 $set
, 他挂载在 vm
原型上,我们先说下其使用方式是: this.$set(this.newTodo,"name", '30')
function set (target, key, val) { if (isUndef(target) || isPrimitive(target) ) { warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); } if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); return val } if (key in target && !(key in Object.prototype)) { target[key] = val; return val } var ob = (target).__ob__; if (target._isVue || (ob && ob.vmCount)) { warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ); return val } if (!ob) { target[key] = val; return val } defineReactive$$1(ob.value, key, val); ob.dep.notify(); return val } 复制代码
通过上面的分析,使用 $set
方法,需要注意如下几点:
- target 不能是
undefined
,null
,string
,number
,symbol
,boolean
六种基础数据类型 - target 不能直接挂载在
Vue
实例对象上, 而且不能直接挂载在rootdata
属性上
$set
最终调用 defineReactive$$1(ob.value, key, val);
方法去动态添加属性, 并且给该属性添加 getter
, setter
动态添加的属性,同样也需要动态更新视图,则是调用 ob.dep.notify();
方法来动态更新视图
总结
- 如果
data
属性是一个Object
, 则将其将其进行转换,主要是做如下两件事情:
- 给对象添加一个
__ob__
的属性, 其是一个Observer
对象
- 遍历
data
的说有属性('key'), 通过Object.defineProperty
设置其getter
和setter
来进行数据拦截
- 如果
data
(或者子属性)是一个Array
, 则将其原型转换成arrayMethods
(基于Array.prototype
原型创建的一个新的对象,但是重新定义了 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse')七个方法,来进行对Array
的数据拦截(这也就是Vue 对数组操作,只有这七个方法能实现双向绑定的原因)
在这篇文章我们已经分析了 Vue 响应式原理 , 我们接下来会继续分析 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所需修改
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 前端架构之vue+axios 前端实现登录拦截(路由拦截、http拦截)
- react离开页面,自定义弹框拦截,路由拦截
- Springboot整合Hibernate拦截器时无法向拦截器注入Bean
- 基于原生fetch封装一个带有拦截器功能的fetch,类似axios的拦截器
- SpringMVC拦截器
- IOS 拦截器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。