vue的响应式原理
vue的数据代理原理
数据代理:将 data 数据代理 this 上,可以通过 this 直接访问
- 将data中的数据赋值给变量
data
和this.data
- 通过
Object.keys
方法 遍历 赋值后的data
中的所有属性,提取到所有的属性名为一个数组 - 遍历这个数组,取出属性名,对属性名调用数据代理
_proxy
方法 - 数据代理方法:通过
Object.defineProperty
给this
(实列vm)添加属性名,值定义了get
和set
方法 get
和set
方法中都是对this._data
, 也就是原数据进行读写操作- 所以将来就可以直接通过
this
访问到data
中的数据,操作this
上的数据,实际操作还是原data
数据
function MVVM(options) {
// options就是配置对象。将这个对象添加到MVVM的实列上
this.$options = options;
// this.$options.data 将实列上的配置对象中的data赋值给this._data 和data
var data = (this._data = this.$options.data);
// 保存当前的this,为了将来可以在其他函数中使用实例对象
var me = this;
// 数据代理最终结果:可以通过this直接访问(读、写)data中的数据
// 1. Object.keys(data) 提取对象所有属性名成为数组
// 2. 对数组进行遍历,提取每一个属性名,对属性名进行数据代理
Object.keys(data).forEach(function (key) {
// 数据代理的方法
me._proxy(key);
});
observe(data, this);
// 模板解析:解析页面中的插值语法和指令语法
this.$compile = new Compile(options.el || document.body, this);
}
//给MVVM的原型对象添加方法
MVVM.prototype = {
$watch: function (key, cb, options) {
new Watcher(this, key, cb);
},
// 数据代理的方法;key:当前data对象中的属性名
_proxy: function (key) {
// 保存当前的this:指向实列对象
var me = this;
// 通过Object.defineProperty方法给this添加新属性
// 设置属性的元属性(定义属性的读取和设置的方法)
// 读取时实际上读取的原属性data数据的值
// 设置时实际上 设置的原属性data数据的值
Object.defineProperty(me, key, {
configurable: false,//禁止修改描述和删除
enumerable: true,//遍历
get: function proxyGetter() {
// get 是读取:所以读取的时候还是读取的是实列中的data数据原数据
return me._data[key];
},s
set: function proxySetter(newVal) {
// set 是修改:所以修改的时候还是修改的是实列中的data数据原数据
//newVal 是要修改的值
me._data[key] = newVal;
},
});
},
};
vue的模板解析
模板解析:
- 将el所有子节点添加文档碎片节点中;
- 解析文档碎片节点的插值语法和指令语法;
- 取出所有子节点,进行遍历;
- 判断子节点是否是元素节点
- 如果是元素节点,那么就要解析指令
- 取出当前节点所有属性节点,进行遍历,判断属性名是否v-开头(是否是指令属性)
- 如果是v-开头,还要判断属性是否是事件指令(on开头)
- 如果是on开头,则就是事件指令,最终给元素绑定事件,且通过bind方法将事件回调函数的this改为vm(实例)
- 如果不是on开头,则就是普通指令(v-text、v-html)最终会调用相应更新数据的updator方法更新DOM元素内容,
- 最后处理完成的指令属性,会被移除掉
- 如果不是v-开头,就是普通指令,那就看下一个属性
- 如果不是元素节点,要判断是否是文本节点 且 里面文本内容 是否 有插值语法
- 如果有插值语法,就取出当前节点所有文本节点,进行遍历,将遍历得到的插值语法中的变量去实列
data
中查找这个变量相对应的值;最后将这个值赋值给当前的这个节点元素 - 如果没有插值语法,就会看当前子节点是否还有子节点,接着进行递归调用,对子节点的子节点在进行处理
- 如果有插值语法,就取出当前节点所有文本节点,进行遍历,将遍历得到的插值语法中的变量去实列
- 如果是元素节点,那么就要解析指令
- 将解析后文档碎片节点添加到el(页面)中生效;
###vue的响应式原理详细版
- 数据代理:将
data
中的数据代理this
上,访问数据更方便
-
详细版
- 将
data
数据赋值给变量data
和this._data
上,这种数据称为原数据 - 通过
Object.keys
方法提取原数据的所有属性名成为一个数组 - 遍历这个数据,调用
_proxy
对每一个属性名进行数据代理 - 所谓数据:就是通过
Object.defineProperty
方法给this
定义这个属性,设置属性的元属性(属性描述符) - 元属性中定义数据的是否可枚举,是否可以重新配置,以及
get
属性读取方法 和set
属性的设置方法 get
和set
方法本质上都是对之前的原数据进行操作- 到此所有数据都会定义在
this
上,所以可以通过this
直接访问data
中数据了
- 将
- 数据劫持:重新定义
data
原数据上的属性,将其定义响应式属性
-
详细版
- 调用
observe
方法开始数据劫持,传入data
原数据 - 判断数据是否是对象或数组,是的话才会开始真正的工作
new Obverser(data)
- 内部通过
Object.keys
方法提取原data
数据的所有属性名成为一个数组 - 遍历这个数组,提取属性名和属性值,调用
defineReactive
方法将其定义成响应式 - 先
new Dep()
生成dep
对象,这个dep
可以做两件事- 可以通过建立响应式联系(
dep.depend
) - 可以触发响应式更新(
dep.notify
) - 每个响应式数据都会产生一个自己的
dep
,它是唯一的
- 可以通过建立响应式联系(
- 递归调用
observe
函数,对属性值进行处理- 它的目的为了确保所有数据(对象上对象上的数据)都变成响应式
- 通过
Object.defineProperty
方法给原数据data
重新定义属性,定义其属性get
和set
方法,get
方法会返回原数据(内部会有dep.depend
方法)set
方法设置新值,通过调用observe
方法将新值重新定义成响应式(内部会有dep.notify
方法)- 确保将来所有
data
数据都是响应式(不管是之前的,还是新添加的)
- 确保将来所有
- 此时
data
数据就全部重新定义了,定义成响应式,响应式的关键点还是看下一步
- 调用
- 模板解析: 解析模板中插值语法和指令语法
-
详细版
- 从
new Compile()
开,一共三个步骤 - 将 el 的所有子节点添加到文档碎片节点中
- 遍历 el 的子节点添加文档碎片中
- 一旦节点添加文档碎片中,节点就会自动从 DOM 消失
- 解析、编译文档碎片的模板代码
- 取出子节点进行遍历
- 判断节点是否是元素节点
- 如果是元素节点,进行解析元素的指令语法
- 取出元素的所有属性节点,进行遍历
- 判断是否是指令属性(v-)
- 如果是,还要判断是否是事件指令(on)
- 如果是,就是事件指令,给元素绑定事件和相应的回调函数,回调函数通过 bind 方法改变 this 指向为 vm
- 如果不是,就是一般指令
- compileUtil[‘xxx’] --> bind --> xxxUpdaterFn --> 操作 DOM 元素
- 比如:v-text 操作元素 textContent 属性,值为表达式的值
- 注意最后会 new watcher(),会将更新数据的 updater 函数传入,这个 new Watcher 建立响应式联系的入口
- compileUtil[‘xxx’] --> bind --> xxxUpdaterFn --> 操作 DOM 元素
- 解析完指令后,会将指令属性给移除掉
- 如果不是,就不处理这个属性
- 如果是,还要判断是否是事件指令(on)
- 再判断节点是否是文本节点且包含插值语法
- 满足条件,进行解析文本的插值语法
- 和 v-text 指令解析类似,都是最终调用 textUpdater 方法去更新元素的 textContent 的值
- 注意最后会 new watcher()
- 满足条件,进行解析文本的插值语法
- 最后判断当前节点是否有子节点,如果有就要递归调用,对所有子节点编译
- 目的:为了解析模板中所有节点
- 将解析后的文档碎片代码添加到 el 中生效
- 从
-
new Watcher()是如何建立起响应式联系的
- new Watcher() 此时会去原数据上读取表达式值
- 会触发数据劫持阶段给原数据属性绑定的 get 方法
- get 方法中会调用 dep.depend(),此时就会建立响应式联系
- 响应式联系:会在 dep 中保存相应的 watcher,wacher 中保存相应的 dep
- dep 中保存相应的 watcher 的目的:为了将来更新数据时,调用 dep 中的所有 watcher.update 方法去更新用户界面
- wacher 中保存相应的 dep 的目的:为了防止 dep 重复保存相同的 watcher
-
到此建立响应式联系
-
响应式触发:
- 当更新数据时
- 首先触发 数据代理阶段给 this 设置的属性的 set 方法,内部实际操作的是 data 原数据
- 操作原数据,又会触发 数据劫持阶段给原数据属性绑定的 set 方法,
- set 方法中会更新数据,同时调用 dep.notify 方法
- dep.notify 方法内部会遍历所有保存的 watcher 去调用更新用户界面的方法
- 从而实现,数据发生变化,页面也发生变化,达到响应式
vue的响应式原理简单版
1数据代理:将 data
中的数据代理 this
上,访问数据更方便
- 遍历所有原数据中属性进行数据代理,通过
Object.defineProperty
方法给this
添加属性,定义其get
和set
方法,内部实际读取、设置还是原数据的值
2.数据劫持:重新定义 data
原数据上的属性,将其定义响应式属性
- 内部会递归遍历所有
data
数据,将其定义成响应式 - 每个响应式数据都会产生一个自己的
dep
,它是唯一的,和响应式关系很大 - 通过
Object.defineProperty
方法给原数据data
重新定义属性,定义其属性get
和set
方法
3.模板解析: 解析模板中插值语法和指令语法
-
将 el 的所有子节点添加到文档碎片节点中
-
解析、编译文档碎片的模板代码
- 解析分为指令语法和插值语法
- 元素节点解析指令语法,文本节点解析插值语法
- 解析指令语法,会根据具体的指令进行相应的解析
- v-on 给元素绑定事件
- v-text 会设置元素 textContent
- 解析插值语法,就会给元素设置 textContent
- 最终:解析处理事件指令以外的其他指令和插值语法,会 new Watcher(), 这个就会建立起响应式联系
-
将解析后的文档碎片代码添加到 el 中生效
-
到此建立响应式联系
-
响应式触发:
- 当更新数据时
- 首先触发 数据代理阶段给 this 设置的属性的 set 方法,内部实际操作的是 data 原数据
- 操作原数据,又会触发 数据劫持阶段给原数据属性绑定的 set 方法,
- set 方法中会更新数据,同时调用 dep.notify 方法
- dep.notify 方法内部会遍历所有保存的 watcher 去调用更新用户界面的方法
- 从而实现,数据发生变化,页面也发生变化,达到响应式
双向数据绑定原理
v-model 指令如何做到双向数据绑定
- 给元素绑定 value 属性,值为表达式的值,此时数据 Model --> View
- 继续给元素绑定 input 事件,在事件回调函数中通过 e.target.value 收集用户输入的数据
- 更新原 data 数据为 e.target.value,此时数据 View --> Model
- 从而实现双向数据绑定
- 原理:给元素绑定 value 属性和 input 事件来完成的