内容简介:上一篇文章,大概的讲解了Vue实例化前的一些配置,如果没有看到上一篇,通道在这里:在上一篇的结尾,我说这一篇后着重讲一下在这里,根据源码,我决定在给大家讲一遍,看看和大家平时自己看的,有没有区别,如果有遗漏的点,欢迎评论
上一篇文章,大概的讲解了Vue实例化前的一些配置,如果没有看到上一篇,通道在这里: Vue 源码解析 - 实例化 Vue 前(一)
在上一篇的结尾,我说这一篇后着重讲一下 defineReactive 这个方法,这个方法,其实就是大家可以在外面看见一些文章对 vue 实现数据双向绑定原理的过程。
在这里,根据源码,我决定在给大家讲一遍,看看和大家平时自己看的,有没有区别,如果有遗漏的点,欢迎评论
正文
先来一段 defineReactive 的源码:
//在Object上定义反应属性。 function defineReactive ( obj, key, val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } var getter = property && property.get; if (!getter && arguments.length === 2) { val = obj[key]; } var setter = property && property.set; 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 (process.env.NODE_ENV !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); } 复制代码
在讲解这段源码之前,我想先在开始讲一下 Object 的两个方法 Object.defineProperty() 和 Object.getOwnPropertyDescriptor()
虽然很多前端的大佬知道它的作用,但是我相信还是有一些朋友是不认识的,我希望我写的文章,不只是传达vue内部实现的一些精神,更能帮助一些小白去了解一些原生的api。
defineProperty
在 MDN 上的解释是:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。 复制代码
这里,其实就是用来实现数据双向绑定的核心之一,主要做的事情就是数据的更新, Object.defineProperty() 最多接收三个参数: obj , prop , descriptor :
obj:
要在其上定义属性的对象。 复制代码
prop:
要定义或修改的属性的名称。 复制代码
descriptor:
将被定义或修改的属性描述符。 复制代码
返回值:
被传递给函数的对象。 复制代码
在这里要注意一点: 在ES6中,由于 Symbol类型的特殊性,用Symbol类型的值来做对象的key与常规的定义或修改不同,而Object.defineProperty 是定义key为Symbol的属性的方法之一。
对象里目前存在的属性描述符有两种主要形式: 数据描述符 和 存取描述符 。 数据描述符 是一个具有值的属性,该值可能是可写的,也可能不是可写的。 存取描述符 是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
数据描述符和存取描述符均具有以下可选键值:
configurable:
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认值: false 复制代码
enumerable:
当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。 默认为 false。 复制代码
数据描述符同时具有以下可选键值:
value:
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为 undefined。 复制代码
writable:
当且仅当该属性的 writable 为 true 时,value 才能被赋值运算符改变。 默认为 false。 复制代码
存取描述符同时具有以下可选键值:
get:
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。 默认为 undefined。 复制代码
set:
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。 默认为 undefined。 复制代码
Object.getOwnPropertyDescriptor()
obj:
需要查找的目标对象 复制代码
prop:
目标对象内属性名称(String类型) 复制代码
descriptor:
将被定义或修改的属性描述符。 复制代码
返回值:
返回值其实就是 Object.defineProperty() 中的那六个在 descriptor 对象中可设置的属性,这里就不废话浪费篇幅了,大家看一眼上面就好 复制代码
defineReactive的参数我就不一一列举的来讲了,大概从参数名也可以知道大概的意思,具体讲函数内容的时候,在细讲。
Dep
var dep = new Dep(); 复制代码
在一进入到 defineReactive 这个函数时,就实例化了一个Dep的构造函数,并把它指向了一个名为dep的变量,下面,我们来看看Dep这个构造函数都做了什么:
var uid = 0; var Dep = function Dep () { this.id = uid++; this.subs = []; }; Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; Dep.prototype.removeSub = function removeSub (sub) { remove(this.subs, sub); }; Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } }; Dep.prototype.notify = function notify () { var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; Dep.target = null; 复制代码
在实例化 Dep 之前,给 Dep 添加了一个 target 的属性,默认值为 null;
Dep在实例化的时候,声明了一个 id 的属性,每一次实例化Dep的id都是唯一的;
然后声明了一个 subs 的空数组, subs 要做的事情,就是收集所有的依赖;
addSub:
从字面意思,大家也可以看的出来,它就是做了一个添加依赖的动作;
removeSub:
其实就是移除了某一个依赖,只不过实现没有在当前的方法里写,而是调用的一个 remove 的方法:
function remove (arr, item) { if (arr.length) { var index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1) } } } 复制代码
这个方法,就是从数组中,移除了某一项;
depend:
添加一个依赖数组项;
notify:
通知每一个数组项,更新每一个方法;
这里 subs 调用了 slice 方法,官方注释是 “ stabilize the subscriber list first ” 字面意思是 “首先稳定订户列表”,这里我不是很清楚,如果知道的大佬,还请指点一下 复制代码
Dep.target在 Vue 实例化之前一直都是 null ,只有在 Vue 实例化后,实例化了一个 Watcher 的构造函数,在调用 Watcher 的 get 方法的时候,才会改变 Dep.target 不为 null ,由于 Watcher 涉及的内容也很多,所以我准备单拿出一章内容,在 Vue 实例化之后去讲解,现在,我们就暂时当作 Dep.target 不为空。
现在, Dep 构造函数讲解的就差不多了,我们继续接着往下看:
var property = Object.getOwnPropertyDescriptor(obj, key); 复制代码
方法返回指定对象上一个自有属性对应的属性描述符并赋值给property;
if (property && property.configurable === false) { return } 复制代码
我们要实现数据双向绑定的时候,要看当前的 object 上面是否有当前要实现数据双向绑定的这个属性,如果没有,并且 configurable 为 false,那么就直接退出该方法。
在上面我们介绍过 configurable 这个属性,如果它是 flase ,说明它是不允许被更改的,那么就肯定不支持数据双向绑定了,那肯定是要退出该方法的。
var getter = property && property.get; if (!getter && arguments.length === 2) { val = obj[key]; } 复制代码
获取当前该属性的 get 方法,如果没有该方法,并且只有两个参数(obj 和 key),那么 val 就是直接从这个当前的 obj 里面获取。
var setter = property && property.set; 复制代码
获取当前属性的 set 方法。
var childOb = !shallow && observe(val); 复制代码
判断是否要浅拷贝,如果传的是 false ,那么就是要进行深拷贝,这个时候,就需要把当前的值传递给 observe 的方法:
observe
function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob } 复制代码
在 defineReactive 中,调用 observe 方法,只传了一个参数,所以这里是只有 value 一个值的,第二个值其实就是一个 boolean 值,用来判断是否是根数据;
function isObject (obj) { return obj !== null && typeof obj === 'object' } 复制代码
首先,要检查当前的值是不是对象,或者说当前的值的原型是否在 VNode 上,那就直接 return 出当前方法, VNode 是一个构造函数,内容比较多,所以这一章暂时不讲,接下来单独写一篇去讲 VNode。
var hasOwnProperty = Object.prototype.hasOwnProperty; function hasOwn (obj, key) { return hasOwnProperty.call(obj, key) } 复制代码
这里用来判断对象是否具有该属性,并且对象上的该属性原型是否指向的是 Observer ;
如果是,说明这个值是之前存在的,那么变量 ob 就等于当前观察的实例;
如果不是,则是做如下判断:
var shouldObserve = true; function toggleObserving (value) { shouldObserve = value; } 复制代码
shouldObserve 用来判断是否应该观察,默认是观察;
var _isServer; var isServerRendering = function () { if (_isServer === undefined) { /* istanbul ignore if */ if (!inBrowser && !inWeex && typeof global !== 'undefined') { // detect presence of vue-server-renderer and avoid // Webpack shimming the process _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; } else { _isServer = false; } } return _isServer }; 复制代码
是否支持服务端渲染;
Array.isArray(value) 复制代码
当前的值是否是数组;
isPlainObject(value) 复制代码
用来判断是否是Object;具体代码上一篇文章当中有描述,入口在这里: Vue 源码解析 - 实例化 Vue 前(一)
Object.isExtensible(value) 复制代码
判断一个对象是否是可扩展的
value._isVue 复制代码
判断是否可以被观察到,初始化是在 initMixin 方法里初始化的,这里暂时先不做太多的介绍。
这么多判断的总体意思,就是用来判断,当前的值,是否是被观察的,如果没有,那么就创建一个新的出来,并赋值给变量 ob;
asRootData 如果是 true,并且 ob 也存在的话,那么就给 vmCount 加 1;
最后返回一个 ob。
接下来,开始数据双向绑定的核心代码部分了:
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { }, set: function reactiveSetter (newVal) { } }); 复制代码
首先,要确保要监听的该属性,是可枚举、可修改的的;
get
var value = getter ? getter.call(obj) : val; 复制代码
先前,在前面把当前属性的 get 方法,传给 getter 变量,如果 getter 变量存在,那么就把当前的 getter 的 this 指向当前的 obj 并传给 value 变量;如果不存在,那么就把当前方法接收到的 val 参数传给 value 变量;
if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value 复制代码
每次在 get 的时候,判断 Dep.target 是否为空,如果不为空,那么就去添加一个依赖,调用实例对象 dep 的 depend 方法,这里在 Watcher 的构造函数里,还做了一些特殊处理,等到讲解 Watcher 的时候,我会把这里在带过去一起讲一下。
反正大家记着,在 get 的时候添加了一个依赖就好。
如果是存在子级的话,并且给子级添加一个依赖:
function dependArray (value) { for (var e = (void 0), i = 0, l = value.length; i < l; i++) { e = value[i]; e && e.__ob__ && e.__ob__.dep.depend(); if (Array.isArray(e)) { dependArray(e); } } } 复制代码
如果当前的值是数组,那么我们就要给这个数组添加一个监听,因为本身 Array 是不支持 defineProperty 方法的;
所以在这里,作者给所有的数组项,添加了一个依赖,这样每一个数组选项,都有了自己的监听,当它被改变的时候,会根据监听的依赖,去做对应的更新。
set
var value = getter ? getter.call(obj) : val; 复制代码
这里,和 get 时候一样,获取当前的一个值,如果不存在,就返回函数接收到的值;
if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); 复制代码
如果当前值和新的值一样,那就说明没有什么变化,这样就不需要改,直接 return 出去;
如果是在开发环境下,并且存在 customSetter 方法,那么就调用它;
如果当前的属性存在 set 方法,那么就把 set 方法指向 obj,并把 newVal 传过去;
如果不存在,那么就直接把值给覆盖掉;
如果不是浅拷贝的话,那么就把当前的新值传给 observe 方法,去检查是否已经被观察,并且把新的值覆盖到 childOb 上;
最后调用 dep 的 notify 方法去通知所有的依赖进行值的更新。
概括
到这里,基本上 vue 实现的数据双向绑定的原理,抛析的就差不多了,但是整体涉及的东西比较多,可能看起来会比较费劲一些,这里我概括一下:
- 每次在监听某一个属性时,要先实例化一个队列 Dep,负责监听依赖和通知依赖;
- 确认当前要监听的属性是否存在,并且是可修改的;
- 如果没有接收到参数 val,并且参数只接收到2个,那么就直接把 val 设置成当前的属性的值,不存在就是 undefined;
- 判断当前要监听的值是需要深拷贝还是浅拷贝,如果是深拷贝,那么就去检查当前的值是否被监听,没有被监听,那么就去实例化一个监听对象;
- 在调用 get 方法,获取到当前属性的值,不存在就接收调用该方法时接收到的值;
- 检查当前的队列,要对哪一个 obj 进行变更,如果存在检查的目标的话,那就添加一个依赖;
- 如果存在观察实例的话,在去检查一下当前的值是否是数组,如果是数组的话,那么就做一个数组项的依赖检查;
- 在更新值的时候,发现当前值和要改变的值是相同的,那么就不进行任何操作;
- 如果是开发环境下,还会执行一个回调,该回调实在值改变前但是符合改变条件时执行的;
- 如果当前的属性存在 setter 方法,那么就把当前的值传给 setter 方法,并让当前的 setter 方法的 this 指向当前的 obj,如果不存在,直接用新值覆盖旧值就好;
- 如果是深拷贝的话,就去检查遍当前的值是否被观察,如果没有被观察,就进行观察;(上面大家可能有发现,它已经进行了一次观察,为什么还要执行呢?因为上面是在初始化的时候去观察的,当该值改变以后,比如类型改变,是要进行重新观察,确保如果改变为类似数组的值的时候,还可以进行双向绑定)
- 最后,通知所有添加对该属性进行依赖的位置。
结束语
对应 vue 的数据双向绑定,到这里就总结完了,未来在实例化 vue 对象的地方,会涉及到很多有关数据双向绑定的地方,所以建议大家好好看一下这里。
对于源码,我们了解了作者的思想就好,我们不一定要完全按照作者的写法来写,我们要学习的,是他的编程思想,而不是他的写法,其实好多地方我觉得写的不是很合适,但是我不是很明白为什么要这么做,也许是我水平还比较低,没有涉及到,接下来我会对这些疑问点,进行总结,去研究为什么要这么做,如果不合适,我会在 github 中添加 issues 到时候会把链接抛出来,以供大家参考学习。
最后还是老话,点赞,点关注,有问题了,评论区开喷就好
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- vue 源码学习 - 实例挂载
- 深入剖析Vue源码 - 实例挂载,编译流程
- Vue 源码解析 - 实例化 Vue 前(一)
- 内核通信之 Netlink 源码分析和实例分析
- 内核通信之 Netlink 源码分析和实例分析
- 连载四:PyCon2018|恶意域名检测实例(附源码)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
百度SEO一本通
潘坚、李迅 / 电子工业出版社 / 2015-6 / 59.00元
《百度SEO一本通》通过浅显易懂的叙述方式,以及大量的图示,详细介绍了SEO的关键技术要点,对于搜索引擎优化中重要的关键词优化、链接优化,以及百度推广中的推广技巧都进行了详细的介绍。 《百度SEO一本通》共分为11章,首先让大家了解SEO存在的原因,然后对网页、网站、空间和程序与SEO的关系展开了细节上的讨论,最后几章深入介绍了百度推广的相关概念、设置、技巧和实操,让读者可以轻松上手操作,易......一起来看看 《百度SEO一本通》 这本书的介绍吧!