写在最前:本文转自掘金
Vue2.X响应式原理
缺点说明
- 代理对象只对初始的属性有监听作用,而对新增的属性无效
- 在原对象上代理,而proxy会生成代理对象
- 仅代理对象属性,而proxy代理整个对象
1. defineProperty 的应用
语法:Object.defineProperty(obj, property, descriptor)
- obj 绑定属性的目标对象
- property 绑定属性名
- descriptor 属性描述(配置),且此参数本身为一个对象
- 属性值1:value,设置属性默认值
- 属性值2:writable,设置属性是否能够修改
- 属性值3: enumerable,设置属性是否可以枚举,即是否允许遍历
- 属性值4: confingurable,设置属性是否可以删除或编辑
- 属性值5:get,获取属性的值
- 属性值6:set,设置属性的值
在Vue2.X 响应式中使用到了 defineProperty
进行数据劫持,所以我们必须要对它有一定了解,我们先简单模拟下Vue中的data
<body>
<div id="app"></div>
<script>
// 将 data 对象上的属性 msg 代理到 vm 对象上,在对 vm 对象数据做修改时,会同步修改 data 数据并作出页面渲染
// 为了避免循环调用,setter 与 getter 方法名不可与属性名相同
// 模拟Vue的data
let data = {
msg: ' '
};
// 模拟 Vue 实例
let vm = {}
// 对vm 的msg进行数据劫持
Object.defineProperty(vm, 'msg', {
// 获取数据
get() {return data.msg},
// 设置set
set(newValue){
// 如果传入的值相等就不用修改
if(newValue === data.msg) return
// 修改数据
data.msg = newValue
document.querySlector('#app').textContent = data.msg
}
})
vm.msg = '123'
</script>
</body>
在控制台上修改vm.msg 数据是响应式的
2. defineProperty 修改多个参数为响应式
上面代码只能修改一个属性,实际上我们会有多个属性,所以需要定义个方法把data 中的数据进行遍历劫持。
<body>
<div id="app"></div>
<script>
// 模拟 Vue的data
let data={
msg: 'info',
age: 12,
}
// 模拟Vue 实例
let vm = {}
// 把多个属性转化响应式
function proxyData() {
// 把data中每一项都[msg, age]拿出来操作
Object.key(data).forEach((key)=>{
// 对vm 的属性进行数据劫持
Object.defineProperty(vm, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// 获取数据
get() {return data[key]},
// 设置set
set(newValue){
// 如果传入的值相等就不用修改
if(newValue === data[key]) return
// 修改数据
data[key]= newValue
document.querySlector('#app').textContent = data[key]
}
})
})
}
proxyData(data)
</script>
</body>
6. 模拟Vue的响应式原理
这里来实现一个小型简单的Vue 主要有一下功能
- 接受初始化的参数,这里只举几个简单的例子
el data options
- 通过私有方法
_proxyData
把data
注册到 Vue 中,转成getter setter
- 使用
observer
把data 中的属性转换为响应式添加到自身身上 - 使用
observer
对象监听data
的所有属性变化来,通过观察者模式更新视图 - 使用
compiler
编译元素节点上面指令 和 文本节点
1. Vue.js
在这里 获取到 el data
通过 _proxyData
把data的属性 注册到Vue 并转成 getter setter
/* vue.js */
class Vue{
constructor(options) {
// 获取到传入的对象 没有默认空对象
this.$options = options || {}
// 获取 el
this.$el = typeof options.el == 'string' ? document.querySelector(options.el) : options.el
// 获取 data
this.$data = options.data || {}
// 调用 _proxyData 处理 data中属性
this._proxyData(this.$data)
}
// 把data 中的属性注册到 Vue
_proxyData(data){
Object.keys(data).forEach((key)=>{
// 进行数据劫持,把每个data的属性 添加到 Vue 转化为 getter setter 方法
Object.defineProperty(this, key, {
// 设置可以枚举
enumerable: true,
// 设置可以配置
configurable: true,
// 获取数据
get() {
return data[key]
},
set(newValue){
// 判断新值和旧值是否相等
if(newValue === data[key]) return
// 设置新值
data[key] = newValue
}
})
})
}
}
2. observer.js
在这里把 data中的属性变为响应式加载自身身上,还有一个主要功能就是观察者模式在 第4.dep.js 会有详细的使用
/*observer.js*/
class Observer {
constructor(data){
// 用来遍历 data
this.walk(data)
}
// 遍历 data 转为响应式
walk(data){
// 判断data是否为空 和 非对象
if(!data || typeof data !== 'object') return
// 遍历 data
Object.keys(data).forEach((key)=>{
// 转为响应式
this.defineReactive(data, key, data[key])
})
}
// 转为响应式
// 要注意 和 vue.js 写法不同的是,vue.js中是将属性给了Vue转为getter setter
// 这里是将 data中的属性转为getter setter
defineReactive(obj, key, value){
// 如果是对象类型 的 也调用walk 变成响应式,不是对象类型的直接在walk会被return
this.walk(value)
// 保存一下 this
const self = this
Object.defineProperty(obj, key, {
enumerable: true, // 设置可枚举
configurable: true, // 可配置
// 获取值
get(){
return value
},
// 设置值
set(newValue){
// 判断旧值和新值是否相等
if(newValue === value) return
// 设置新值
value = newValue
// 注意,如果newValue 是对象的话,还需要将对象里的属性设置为响应式
self.walk(newValue)
}
})
}
}
在html中引入的话注意下顺序
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
然后在vue.js中使用observer
class Vue{
constructor(options){
...
// 使用 Observer 把data中的数据转为响应式
new Observer(this.$data)
}
// 把 data 中的属性注册到 Vue
_proxyData(data){
...
}
}
看到这里为什么做了两个重复性的操作呢?重复两次把data的属性转为响应式
在observer.js中把data的所有属性加到data自身变为响应式转成getter sertter 方式
在vue.js中 也把data的所有属性加到Vue上,是为了以后方便操作可以用Vue的实例直接访问到或者在Vue中使用this访问
现在来使用一下例子
<body>
<div id="app"></div>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: '123',
age: 21,
},
})
</script>
</body>
watermark13.jpg
这样在Vue 和 $data 中都存在了 所有的data属性了,并且是响应式的
3. compiler.js
comilper.js 在这个文件里实现对文本节点和 元素节点指令编译,主要是为了举例子,这个写的很简单,指令主要实现 v-text v-model
/*compiler.js*/
class Compiler {
// vm 指Vue实例
constructor(vm){
// 拿到 vm
this.vm = vm
// 拿到 el
this.el = vm.$el
// 编译模板
this.compile(this.el)
}
// 编译模板
compile(el){
// 获取子节点 如果使用 forEach遍历就把伪数组转为真数组
let childnodes = [...el.childNodes];
childenodes.forEach((node)=>{
// 根据不同的节点类型进行编译
if(this.isTextNode(node)){
// 编译文本
this.compileText(node)
} else if(this.isElementNode(node)){
// 元素节点
this.compileElement(node)
} //判断是否还存在子节点 需考虑递归
if(node.childNodes && node.childNodes.length) {
// this.compile(node)
}
})
}
// 编译文本节点(简单的实现)
compileText(node){
// 核心思想利用正则表达式把{{}}去掉找到里面的变量
// 再去Vue找到这个变量赋值给node.textContent
let reg = /\{\{(.+?)\}\}/
// 获取节点的文本内容
let val = node.textContent
// 判断是否含有{{}}
if(reg.text(val)){
// 获取分组一 也就是{{}}里面的内容 去掉前后空格
let key = RegExp.$1.trim()
// 进行替换再赋值给node
node.textContent = val.replace(reg, this.vm[key])
}
}
// 编译元素节点这里只处理指令
compileElement(node){
// 获取到元素节点上面的所有属性进行遍历
[...node.attributes].forEach((attr)=>{
// 获取属性名
let attrName = attr.name
// 判断是否是 v- 开头的指令
if(this.isDirective(attrName)){
// 去除 v- 方便操作
attrName = attrName.substr(2)
// 获取指令的值就是 v-text = 'msg' 中msg
// msg 作为key 去 vue 找这个变量
let key = attr.value
// 指令操作 执行指令方法
// vue指令很多为了避免大量的 if 判断这里就写了个 update 方法
this.update(node, key, attrName)
}
})
}
// 添加指令方法 并且执行
update(node, key, attrName){
// 比如添加 textUpdater 就是用来处理 v-text 方法
// 我们应该就内置一个 textUpdater 方法进行调用
// 加个后缀,加什么无所谓,但要定义相应的方法
let updateFn = this[attrName + 'Updater']
// 如果存在这个内置方法 就可以调用了
updateFn && updateFn(node, key, this.vm[key])
}
// 提前写好 相应的指定方法 比如 v-text
// 使用的时候 和 Vue的一样
textUpdater(node, key, value){
node.textContent = value
}
// v-model
modelUpdater(node, key, value){
node.value = value
}
// 判断元素的属性是否是 Vue 指令
isDirective(attr){
return attr.startsWith('v-')
}
// 判断是否是元素节点
isElementNode(node){
return node.nodeType == 1
}
// 判断是否是文本节点
isTextNode(node){
return node.nodeType == 3
}
}
4. dep.js
写一个Dep类,它相当于观察者中的目标对象,每个响应式属性都会创建这么一个Dep对象,负责收集该依赖属性的Watcher对象(是在使用响应式数据的时候做的操作)
当我们对响应式属性在 setter 中进行更新的时候,会调用Dep 中的 notify 方法发送更新通知
然后去调用 Watcher中的update实现视图的更新操作(是当数据发生变化时候去通知观察者调用观察者的update更新视图)
总的来说 Dep(这里指目标对象)中负责收集依赖,添加观察者(Wather),然后在setter数据更新的时候通知观察者。
先写Dep类
/*dep.js*/
class Dep{
constructro(){
// 储存观察者
this.subs = []
}
// 添加观察者
addSub(sub){
// 判断观察者是否存在 和 是否拥有update方法
if(sub && sub.update){
this.subs.push(sub)
}
}
// 通知方法
notify(){
// 触发每个观察者的更新方法
this.subs.forEach((sub)=>{
sub.update()
})
}
}
在observer.js中使用Dep
在get中添加Dep.target
(观察者)
在set中触发notify(通知)
/* observer.js */
class Oberver {
...
walk(data){...}
definReative(obj, key, value){
...
// 创建Dep对象
let dep = new Dep()
Object.defineProperty(obj, key, {
...
// 获取值
get(){
// 在这里添加观察者对象 Dep.target 标识观察者
Dep.target && dep.addSub(Dep.target)
return value
},
// 设置值
set(newValue) {
if(newValue === value) return
value = newValue
self.walk(newValue)
// 触发通知更新视图
dep.notify()
}
})
}
}
5. watcher.js
watcher.js的作用是,数据更新收到通知后,调用update进行更新
/* watcher.js*/
class Watcher{
constructor(vm, key, cb){
// vm是Vue实例
this.vm = vm
// key是 data中的属性
this.key = key
// cb 是回调函数 更新视图的具体方法
this.cb = cb
// 把观察者的存放在 Dep.target
Dep.target = this
// 旧数据 更新视图的时候要进行比较
// 还有一点就是 vm[key] 这个时候就触发 get方法
// 之前在get 把观察者 通过dep.addSub(Dep.target) 添加到了 dep.subs中
this.oldValue = vm[key]
// Dep.target 就不用存在了 因为上面的操作已经存好了
Dep.target = null
}
// 观察者中的必备方法 用来更新视图
update(){
// 获取新值
let newValue = this.vm[this.key]
// 比较旧值和新值
if(newValue === this.oldValue) return
// 调用具体的更新方法
this.cb(newValue)
}
}
那么去哪创建Watcher呢?还记得在compiler.js中对文本节点编译操作吗
在编译完文本节点后,在这里添加一个Wather
还有 v-text v-model 指令,当编译的事元素节点 就添加一个Watcher
/* compiler.js */
class Compiler{
// vm 指 Vue实例
constructor(vm){...}
compile(el){...}
// 编译文本节点
compileText(node){
...
if(reg.test(val)){
let key = RegExp.$1.trim()
node.textContent = val.replace(reg, this.vm[key])
// 创建观察者
new Watcher(this.vm, key, newValue =>{
node.textContent = newValue
})
}
}
...
}
8. 五个文件代码
/*Vue */
class Vue {
constructor(options) {
// 获取到传入的对象 没有默认空对象
this.$options = options || {}
// 获取 el
this.$el = typeof options.el == 'string' ? document.querySelector(options.el) : options.el
// 获取 data
this.$data = options.data || {}
// 调用 _proxyData 处理 data中属性 转为响应式,添加到Vue身上
this._proxyData(this.$data)
// 把data中的属性转为响应式,添加到自身身上
new Observer(this.$data)
// 编译模板
new Compiler(this)
}
// 把data 中的属性注册到 Vue
_proxyData(data) {
Object.keys(data).forEach((key) => {
// 进行数据劫持,把每个data的属性 添加到 Vue 转化为 getter setter 方法
Object.defineProperty(this, key, {
// 设置可以枚举
enumerable: true,
// 设置可以配置
configurable: true,
// 获取数据
get() {
return data[key]
},
set(newValue) {
// 判断新值和旧值是否相等
if (newValue === data[key]) return
// 设置新值
data[key] = newValue
}
})
})
}
}
/*Observer */
class Observer {
constructor(data) {
// 用来遍历 data
this.walk(data)
}
// 遍历 data 转为响应式
walk(data) {
// 判断data是否为空 和 非对象
if (!data || typeof data !== 'object') return
// 遍历 data
Object.keys(data).forEach((key) => {
// 转为响应式
this.defineReactive(data, key, data[key])
})
}
// 转为响应式
// 要注意 和 vue.js 写法不同的是,vue.js中是将属性给了Vue转为getter setter
// 这里是将 data中的属性转为getter setter
defineReactive(obj, key, value) {
// 如果是对象类型 的 也调用walk 变成响应式,不是对象类型的直接在walk会被return
this.walk(value)
// 保存一下 this
const self = this
// 创建 Dep 对象
let dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true, // 设置可枚举
configurable: true, // 可配置
// 获取值
get() {
// 编译模板的时候会创建炸弹,创建炸弹的时候会调用此方法,并且传入Dep.target(炸弹)
// 在这里添加观察者对象 Dep.target 表示观察者
Dep.target && dep.addSub(Dep.target)
return value
},
// 设置值
set(newValue) {
// 判断旧值和新值是否相等
if (newValue === value) return
// 设置新值
value = newValue
// 注意,如果newValue 是对象的话,还需要将对象里的属性设置为响应式
self.walk(newValue)
// 触发通知 更新视图
dep.notify()
}
})
}
}
/*Compiler */
class Compiler {
// vm 指Vue实例
constructor(vm){
// 拿到 vm
this.vm = vm
// 拿到 el
this.el = vm.$el
// 编译模板
this.compile(this.el)
}
// 编译模板
compile(el){
// 获取子节点 如果使用 forEach遍历就把伪数组转为真数组
let childnodes = [...el.childNodes];
childnodes.forEach((node)=>{
// 根据不同的节点类型进行编译
if(this.isTextNode(node)){
// 编译文本
this.compileText(node)
} else if(this.isElementNode(node)){
// 元素节点
this.compileElement(node)
} // 判断是否还存在子节点 需考虑递归
if(node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译文本节点(简单的实现)
compileText(node){
// 核心思想利用正则表达式把{{}}去掉找到里面的变量
// 再去Vue找到这个变量赋值给node.textContent
let reg = /\{\{(.+?)\}\}/
// 获取节点的文本内容
let val = node.textContent
// 判断是否含有{{}}
if(reg.test(val)){
// 获取分组一 也就是{{}}里面的内容 去掉前后空格
let key = RegExp.$1.trim()
// 进行替换在赋值给node
node.textContent = val.replace(reg, this.vm[key])
// 创建炸弹的时候,会把当前的实例对象(炸弹)挂载在Dep构造函数的原型上,在调用data的get方法自动触发装载当前炸弹,再销毁Dep构造函数原型上的炸弹
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
})
}
}
// 编译元素节点这里只处理指令
compileElement(node){
// 获取到元素节点上面的所有属性进行遍历
[...node.attributes].forEach((attr)=>{
// 获取属性名
let attrName = attr.name
// 判断是否是 v- 开头的指令
if(this.isDirective(attrName)){
// 去除 v- 方便操作
attrName = attrName.substr(2)
// 获取指令的值就是 v-text = 'msg' 中msg
// msg 作为key 去 vue 找这个变量
let key = attr.value
// 指令操作 执行指令方法
// vue指令很多为了避免大量的 if 判断这里就写了个 update 方法
this.update(node, key, attrName)
}
})
}
// 添加指令方法 并且执行
update(node, key, attrName){
// 比如添加 textUpdater 就是用来处理 v-text 方法
// 我们应该就内置一个 textUpdater 方法进行调用
// 加个后缀,加什么无所谓,但要定义相应的方法
let updateFn = this[attrName + 'Updater']
// 如果存在这个内置方法 就可以调用了(当前执行函数的环境不同,所有需要指定方法里的this为当前this)
updateFn && updateFn.call(this,node, key, this.vm[key])
}
// 提前写好 相应的指定方法 比如 v-text
// 使用的时候 和 Vue的一样
textUpdater(node, key, value){
node.textContent = value
// 创建观察者2
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// v-model
modelUpdater(node, key, value){
node.value = value
// 创建观察者
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 这里实现双向绑定 监听input 事件修改 data中的属性
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 判断元素的属性是否是 Vue 指令
isDirective(attr){
return attr.startsWith('v-')
}
// 判断是否是元素节点
isElementNode(node){
return node.nodeType == 1
}
// 判断是否是文本节点
isTextNode(node){
return node.nodeType == 3
}
}
/*Dep */
// 装炸弹的容器类
class Dep {
constructor() {
// 存储观察者
this.subs = []
}
// 添加观察者
addSub(sub) {
// 判断观察者是否存在 和 是否拥有update方法
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 通知方法
notify() {
// 触发每个观察者的更新方法
this.subs.forEach((sub) => {
sub.update()
})
}
}
/*Watcher */
// 炸弹类
class Watcher {
constructor(vm, key, cb) {
// vm 是 Vue 实例
this.vm = vm
// key 是 data 中的属性
this.key = key
// cb 回调函数 更新视图的具体方法
this.cb = cb
// 把观察者的存放在 Dep.target
Dep.target = this
// 旧数据 更新视图的时候要进行比较
// 还有一点就是 vm[key] 这个时候就触发了 get 方法
// 之前在 get 把 观察者 通过dep.addSub(Dep.target) 添加到了 dep.subs中
this.oldValue = vm[key]
// Dep.target 就不用存在了 因为上面的操作已经存好了
Dep.target = null
}
// 观察者中的必备方法 用来更新视图
update() {
// 获取新值
let newValue = this.vm[this.key]
// 比较旧值和新值
if (newValue === this.oldValue) return
// 调用具体的更新方法
this.cb(newValue)
}
}
测试代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
{{msg}} <br />
{{age}} <br />
<div v-text="msg"></div>
<input type="text" v-model="msg" />
</div>
<script src="./dep.js"></script>
<script src="./watcher.js"></script>
<script src="./compiler.js"></script>
<script src="./oberver.js"></script>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el:'#app',
// 两个数据,目标对象 被创建两个
data: {
msg: '没名字的某某人', //3个节点中使用到该属性,目标对象容器里装有3个观察者
age:22
}
})
</script>
</body>
</html>
网友评论