深入了解 Vue 响应式原理(数据拦截)
栏目: JavaScript · 发布时间: 6年前
内容简介:在上一章节我们已经粗略的分析了整个的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 拦截器
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Algorithmic Beauty of Plants
Przemyslaw Prusinkiewicz、Aristid Lindenmayer / Springer / 1996-4-18 / USD 99.00
Now available in an affordable softcover edition, this classic in Springer's acclaimed Virtual Laboratory series is the first comprehensive account of the computer simulation of plant development. 150......一起来看看 《The Algorithmic Beauty of Plants》 这本书的介绍吧!