Vue.js 源码分析(四) 基础篇 响应式原理 data属性

时间:2022-09-26 18:01:04

官网对data属性的介绍如下:

Vue.js 源码分析(四) 基础篇  响应式原理 data属性

意思就是:data保存着Vue实例里用到的数据,Vue会修改data里的每个属性的访问控制器属性,当访问每个属性时会访问对应的get方法,修改属性时会执行对应的set方法。

Vue内部实现时用到了ES5的Object.defineProperty()这个API,也正是这个原因,所以Vue不支持IE8及以下浏览器(IE8及以下浏览器是不支持ECMASCRIPT 5的Object.defineProperty())。

以一个Hello World为例,如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script>
<title>Document</title>
</head>
<body>
<div id="app">{{message}}</div>
<button id="b1">测试按钮</button>
<script>
var app = new Vue({
el:'#app',
data:{                     //data里保存着Vue实例的数据对象,这里只有一个message,值为Hello World!
message:"Hello World!"     
}
})
document.getElementById('b1').addEventListener('click',function(){  //在b1这个按钮上绑定一个click事件,内容为修正app.message为Hello Vue!
app.message='Hello Vue!';
})
</script>
</body>
</html>

显示的内容为:

Vue.js 源码分析(四) 基础篇  响应式原理 data属性

当我们点击测试按钮后,Hello World!变成了Hello Vue!:

Vue.js 源码分析(四) 基础篇  响应式原理 data属性

注:对于组件来说,需要把data属性设为一个函数,内部返回一个数据对象,因为如果只返回一个对象,当组件复用时,不同的组件引用的data为同一个对象,这点和根Vue实例不同的,可以看官网的例子:点我点我

源码分析


Vue实例后会先执行_init()进行初始化(4579行),如下:

writer by:大沙漠 QQ:22969969

  Vue.prototype._init = function (options) {   
var vm = this;
// a uid
vm._uid = uid$3++; /*略*/
if (options && options._isComponent) { //这是组件实例化时的分支,暂不讨论
/*略*/
} else { //根Vue实例执行到这里
vm.$options = mergeOptions(           //这里执行mergeOptions()将属性保存到vm.$options
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);
/*略*/
};

mergeOptions会为每个不同的属性定义不同的合并策略,比如data、props、inject、生命周期函数等,统一放在mergeOptions里面合并,执行完后会保存到Vue实例.$options对象上,例如生命周期函数会进行数组合并处理,而data会返回一个匿名函数:

    return function mergedInstanceDataFn () {     //第1179行,这里会做判断,如果data时个函数,则执行这个函数,当为组件定义data时会执行到这里
// instance merge
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}

接下来返回到_init,_init()会执行initState()函数对props, methods, data, computed 和 watch 进行初始化,如下:

function initState (vm) { //第3303行
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) { //如果定义了data,则调用initData初始化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);
}
}

initData()对data属性做了初始化处理,如下:

function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function' //先获取data的值,这里data是个函数,也就是上面说的第1179行返回的匿名函数,可以看到返回的数据对象保存到了当前实例的_data属性上了
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
"development" !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data); //获取data的所有键名
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length; //键的个数
while (i--) { //遍历data的每个属性
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)) {
"development" !== 'production' && 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); //依次执行proxy,这里对data做了代理 注1
}
}
// observe data
observe(data, true /* asRootData */); //这里对data做了响应式处理,来观察这个data
}

****************我是分隔线****************

注1解释

initData()函数开始的时候把的数据对象保存到了当前实例的_data属性上了,这里是给Vue做了一层代码,当访问每个data属性时将从实例的_data属性上获取对应的属性,Vue内部如下:

var sharedPropertyDefinition = {      //共享属性的一些定义
enumerable: true,
configurable: true,
get: noop,
set: noop
}; function proxy (target, sourceKey, key) { //对data、props做了代理
sharedPropertyDefinition.get = function proxyGetter () { //获取属性
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) { //设置属性
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition); //对target的key属性的get和set做了一层代码
}

例如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script>
<title>Document</title>
</head>
<body>
<div id="app">{{message}}</div>
<button id="b1">测试按钮</button>
<script>
debugger
var app = new Vue({
el:'#app',
data:{
message:"Hello World!"     
}
})
console.log(app._data.message)    //浏览器会输出:(index):19 Hello World!
</script>
</body>
</html>

****************我是分隔线****************

返回到initData()函数,最后会执行observe函数:

var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) { //如果value是个数组
var augment = hasProto
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
this.walk(value); //例子中不是数组,因此调用walk()方法
}
};
Observer.prototype.walk = function walk(obj) { //将obj这个对象,做响应式,处理
var keys = Object.keys(obj); //获取obj对象的所有键名
for (var i = 0; i < keys.length; i++) { //遍历键名
defineReactive(obj, keys[i]); //依次调用defineReactive()函数对象的属性变成响应式
}
};

defineReactive用于把对象的属性变成响应式,如下:

function defineReactive(obj, key, val, customSetter, shallow) {  //把对象的属性变成响应式
var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); //获取obj对象key属性的数据属性
if (property && property.configurable === false) { //如果该属性是不能修改或删除的,则直接返回
return
} var getter = property && property.get; //尝试拿到该对象原生的get属性,保存到getter中
if (!getter && arguments.length === 2) { //如果getter不存在,且参数只有两个
val = obj[key]; //则直接通过obj[ke]获取值,并保存到val中
}
var setter = property && property.set; //尝试拿到该对象原生的set属性,保存到setter中 var childOb = !shallow && observe(val); //递归调用observe:当某个对象的属性还是对象时会进入
Object.defineProperty(obj, key, { //调用Object.defineProperty设置obj对象的访问器属性
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) { //这里就是做依赖收集的事情
dep.depend(); //调用depend()收集依赖
if (childOb) { //如果childOb存在
childOb.dep.depend(); //则调用childOb.dep.depend()收集依赖
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter(newVal) { //做派发更新的事情
var value = getter ? getter.call(obj) : val; //如果之前有定义gvetter,则调用getter获取值,否则就赋值为val
if (newVal === value || (newVal !== newVal && value !== value)) { //如果value没有改变
return //则直接返回,这是个优化错误,当data值修改后和之前的值一样时不做处理
}
if ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal); //再调用observe,传递newVal,这样如果新值也是个对象也会是响应式的了。
dep.notify(); //通知订阅的watcher做更新
}
});
}

当render函数执行转换成虚拟VNode的时候就会执行with(this){}函数,内部访问到某个具体的data时就会执行到这里的访问器控制get函数了,此时会收集对应的渲染watcher作为订阅者,保存到对应属性的dep里面

当修改了data里某个属性时就会除法对应的set访问器控制属性,此时会执行对应的访问其控制的set函数,会执行notify()通知订阅的watcher做更新操作

Vue.js 源码分析(四) 基础篇 响应式原理 data属性的更多相关文章

  1. Vue&period;js 源码分析&lpar;七&rpar; 基础篇 侦听器 watch属性详解

    先来看看官网的介绍: 官网介绍的很好理解了,也就是监听一个数据的变化,当该数据变化时执行我们的watch方法,watch选项是一个对象,键为需要观察的数据名,值为一个表达式(函数),还可以是一个对象, ...

  2. Vue&period;js 源码分析&lpar;十三&rpar; 基础篇 组件 props属性详解

    父组件通过props属性向子组件传递数据,定义组件的时候可以定义一个props属性,值可以是一个字符串数组或一个对象. 例如: <!DOCTYPE html> <html lang= ...

  3. Vue&period;js 源码分析&lpar;三&rpar; 基础篇 模板渲染 el、emplate、render属性详解

    Vue有三个属性和模板有关,官网上是这样解释的: el ;提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标 template ;一个字符串模板作为 Vue 实例的标识使用.模板将会 ...

  4. Vue&period;js 源码分析&lpar;二&rpar; 基础篇 全局配置

    Vue.config是一个对象,包含Vue的全局配置,可以在启动应用之前修改下列属性,如下: ptionMergeStrategies        ;自定义合并策略的选项silent         ...

  5. Vue&period;js 源码分析&lpar;九&rpar; 基础篇 生命周期详解

    先来看看官网的介绍: 主要有八个生命周期,分别是: beforeCreate.created.beforeMount.mounted.beforeupdate.updated   .beforeDes ...

  6. Vue&period;js 源码分析&lpar;八&rpar; 基础篇 依赖注入 provide&sol;inject组合详解

    先来看看官网的介绍: 简单的说,当组件的引入层次过多,我们的子孙组件想要获取祖先组件的资源,那么怎么办呢,总不能一直取父级往上吧,而且这样代码结构容易混乱.这个就是这对选项要干的事情 provide和 ...

  7. Vue&period;js 源码分析&lpar;十一&rpar; 基础篇 过滤器 filters属性详解

    Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化.过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持).过滤器应该被添加在 JavaScrip ...

  8. Vue&period;js 源码分析&lpar;十&rpar; 基础篇 ref属性详解

    ref 被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素:如果用在子组件上,引用就指向组件实例,例如: ...

  9. Vue&period;js 源码分析&lpar;五&rpar; 基础篇 方法 methods属性详解

    methods中定义了Vue实例的方法,官网是这样介绍的: 例如:: <!DOCTYPE html> <html lang="en"> <head&g ...

随机推荐

  1. sql中的&excl;&equals;判断的注意事项

    sql查询中where过滤条件为某字段 colName='xx'时一般不会出什么问题, 但如果想达到不为xx的时候就要注意了,用colName!= 'xx'可能就有问题了,因为该字段可能为空,为nul ...

  2. &lbrack;芯片&rsqb; 4、接口技术&&num;183&semi;实验四&&num;183&semi;串行接口8251A

    目录 一.实验目的和要求... 2 二.实验原理与背景... 3 三.实验具体的内容... 3 四.实验的代码说明... 4 五.实验结果的分析... 6 附录资料 一.实验目的和要求 学会8251芯 ...

  3. Python之路&comma;Day16 - Django 进阶

    Python之路,Day16 - Django 进阶   本节内容 自定义template tags 中间件 CRSF 权限管理 分页 Django分页 https://docs.djangoproj ...

  4. Selenium自动化测试(java语言)

    Selenium介绍 Selenium 1.0 包含 core. IDE. RC. grid 四部分,  selenium 2.0 则是在两位大牛偶遇相互沟通决定把面向对象结构化( OOPP) 和便于 ...

  5. The Swift Programming Language-官方教程精译Swift(5)集合类型 -- Collection Types

    Swift语言提供经典的数组和字典两种集合类型来存储集合数据.数组用来按顺序存储相同类型的数据.字典虽然无序存储相同类型数据值但是需要由独有的标识符引用和寻址(就是键值对).   Swift语言里的数 ...

  6. Linux 按时间批量删除文件(删除N天前文件)

    需要根据时间删除这个目录下的文件,/home/lifeccp/dicom/studies,清理掉20天之前的无效数据. 可以使用下面一条命令去完成: -name "*.*" -ex ...

  7. django之快速分页

    本文介绍djanog两种分页,第一是普通分页,第二是使用haystack全文检索的分页. 1.django自带分页功能,这个功能非常好用.基本知识点:Django提供了数据分页的类,这些类被定义在dj ...

  8. 微信浏览器返回刷新,监听微信浏览器返回事件,网页防复制,移动端禁止图片长按和vivo手机点击img标签放大图片

    以下代码都经过iphone7,华为MT7 ,谷歌浏览器,微信开发者工具,PC端微信验证.如有bug,还请在评论区留言. demo链接:https://pan.baidu.com/s/1c35mbjM ...

  9. hive -- 协同过滤sql语句

    hive -- 协同过滤sql语句 数据: *.3g.qq.com|腾讯应用宝|应用商店 *.91rb.com|91手机助手|应用商店 *.app.qq.com|腾讯应用宝|应用商店 *.haina. ...

  10. appium 原理解析&lpar;转载雷子老师博客&rpar;

    appium 原理解析 原博客地址:https://www.cnblogs.com/leiziv5/p/6427609.html Appium是 c/s模式的appium是基于 webdriver 协 ...