概述
start
- 学习vue2双向数据绑定原理
- 时间仓促,代码纯手写,若有错误欢迎指出。
1. 简单的模拟一下双向数据绑定
var obj = {
a: 1
}
obj.a = 20
console.log('打印它', obj.a)
比如我有一个对象obj,它有一个属性a.我可以设置它的值,也可以获取它的值。没毛病,这种代码我们肯定没少见过。
vue双向数据绑定实现的是什么效果?数据改变了页面改变,页面改变了数据也跟着改变。
那如果想实现它这个效果,该怎么做呢?
简易的模拟一下
<!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>vue双向数据绑定原理学习 by_tomato----1</title>
</head>
<body>
<input type="text" id="inp">
<div id="box"></div>
<script>
var obj = {
a: 1
}
// 值改变了,我们去修改页面
document.getElementById('inp').value = obj.a
document.getElementById('box').innerText = obj.a
obj.a = 'tomato'
// 值改变了,我们去修改页面
document.getElementById('inp').value = obj.a
document.getElementById('box').innerText = obj.a
document.getElementById('inp').addEventListener('input', function (e) {
console.log(e.target.value)
// 页面input值改变了,我们去修改数据
obj.a = e.target.value
// 数据改变了,再去修改页面
document.getElementById('box').innerText = obj.a
})
</script>
</body>
</html>
1.*页面改变* => *改变数据*
我们可以给例如input框添加一个输入事件,当input框的内容改变了,我们修改一下数据。
2.*数据改变* => *改变页面*
每次操作数据的时候,手动更新DOM。
3.效果
可以自己创建一个html页面,复制我的代码运行一下,就会发现基本实现了双向绑定的一个效果。
简易版的弊端: 不适合处理复杂的数据
我上面的代码,只是操作一个数据,稍微还好。但是如果数据量大,结构更复杂,每次数据改变都要手动更新DOM,我裂开了。
其次我并不能精确的掌握数据改变的时机。
2. 初次接触Object.defineProperty
由上面简易版本的弊端,我在想,怎么去监听数据的改变比较合适呢?
这个时候就需要介绍一下 Object.defineProperty
这个方法。
- 翻译一下
define Property
英译:定义属性。 - 注意单词要记住怎么读怎么写,最好是要可以熟练盲写出来。
- 详情可参考 mdn官方文档
英译的意思是,定义属性,其实它的作用就是给属性进行配置。
方法的细节,可以自行查看文档,今天的主角不是它,我这里就直接列一下它使用案例。
Object.defineProperty 基础使用
var obj = {
a: 1
}
var temp
Object.defineProperty(obj, 'a', {
// 注意事项: value 和 get/set 这两对属性,不可以同时使用,会报错。
// value:'3',
get: function () {
console.log('get获取值' + 'a')
return temp
},
set: function (newValue) {
console.log('set 设置值' + 'a')
temp = newValue
}
})
obj.a = 2
// set 设置值a
console.log('你好呀', obj.a)
// get获取值a
// 你好呀 2
我遇到的问题1
// get/set属性,可以用es6的简写方式,第一次遇到可能会觉得有些生疏,其实两个写法都是一样的。
get() {
console.log('get获取值' + 'a')
return temp
},
set(newValue) {
console.log('set 设置值' + 'a')
temp = newValue
}
我遇到的问题2
我最开始接触这个get,set,我不明白,加了这两个东西有什么用呢,这样做了不是和之前一样,去读取值,设置值嘛。
但是你仔细想想,这个地方,我能在get的时候打印get获取值a
,set的时候打印set 设置值a
。那么我在console的地方,是不是可以加其他的代码?
3.给多个元素添加get set
再上述的代码中,我们只给obj中的a属性添加了get和set属性。但是如果我们obj中不仅仅只有一个属性呢,这个时候我们就需要去遍历我们的obj,把每个属性都添加上get和set。
为了方便调用,我们把上面的代码放在一个名为defineReactive的函数中:
function defineReactive(obj, key, data) {
Object.defineProperty(obj, key, {
get() {
console.log('get获取值' + key)
return data
},
set(newValue) {
console.log('set 设置值' + key)
data = newValue
}
})
}
我遇到的问题1
首先知道参数是什么意思。分别是:对象名。属性名。属性值。
我遇到的问题2
它这个地方data用的非常好!最初我们是使用的一个temp变量,来传递值的。
-
现在是形参有一个data,就相当于在当前作用域定义了一个名为data的变量;
-
且data还能接收外部的传参;
-
再者,data会被 get/set函数使用,这就形成了一个闭包,当get/set执行,data会常驻于内存中。
好了不说问题,继续往下思考
开始遍历obj对象,遍历对象有很多方法,我这里就采用for in
,
function defineReactive(obj, key, data) {
Object.defineProperty(obj, key, {
get: function () {
console.log('get获取值' + key)
return data
},
set: function (newValue) {
console.log('set 设置值' + key)
data = newValue
}
})
}
// 这里!
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
defineReactive(obj,key,obj[key])
}
}
遍历这里我又遇到问题了1
Object.hasOwnProperty.call(obj, key)
很眼熟,它是干什么的!不知道。解释一下:在编辑器中打for in
会直接提示这些代码,那么这个方法有什么用,我这里解释一下:
》这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链。
》大白话说就是判断一个属性是不是这个对象自己的,而不是原型链上的,防止遍历到原型链上的属性。
4.如果对象中的属性也是对象呢?
// 例如 d属性 它是对象。我们给a,b,c,d都添加了get set,但是m没有被添加。
var obj = {
a: 1,
b: 2,
c: 3,
d: {
m: 3
}
}
这个时候就需要处理一下属性值为对象的情况,改动如下:
1.首先把遍历对象的方法放在一个函数里面,函数叫observe
2.然后每次添加get,set之前,对每个属性值都observe一下。
3.除此之外,observe之前判断一下传入的值是不是对象,不是对象直接return。
4.其次对设置的新值,也observe一下
完整修改如下:
ps:修改的步骤已经一一对应了
var obj = {
a: 1,
b: 2,
c: 3,
d: {
m: 3
}
}
function defineReactive(obj, key, data) {
observe(obj[key]) // 步骤 2
Object.defineProperty(obj, key, {
get: function () {
console.log('get获取值' + key)
return data
},
set: function (newValue) {
if(data === newValue) return
console.log('set 设置值' + key)
data = newValue
observe(data) // 步骤 4
}
})
}
function observe(obj) { // 步骤 1
if (!obj || typeof obj !== 'object') return // 步骤 3
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
defineReactive(obj, key, obj[key])
}
}
}
observe(obj)
obj.d.m = 333
console.log(obj.d.m)
我遇到的问题1
步骤3用的非常巧妙,既保证了每个属性都会被递归执行observe,又排除了不是对象的属性。如果没有绕过弯,需要捋一下这里的逻辑。
我遇到的问题2:
又来复习js基础知识啦, 数据类型的判断:
-
typeof一般是用来判断基础数据类型的;
-
es6之前,typeof永不报错,返回的都是对应数据类型的,小写字符串形式;
-
typeof是连在一起写的,中间没有驼峰。
-
有几个特殊情况 typeof null =》‘object’ typeof 函数 =》 ‘function’
》 所以啊 !obj || typeof obj !== 'obj'
,!obj用来排除null的情况,typeof obj !== 'obj’排除不是对象的情况(这个地方函数也会被排除。)
我遇到的问题3:
执行obj.d.m = 333
,输出结果为:
执行console.log(obj.d.m)
,输出结果为:
执行上面两行代码,都会打印 “get获取值d ”,一开始以为我的代码写的有问题,但是仔细想想,这个地方没错,就是先读取的obj.d,再去获取d中的属性
5.发布者dep
上面的代码写完了,我们给对象中的每一个属性都添加了get set (暂时我们先不考虑数组的情况)。
那么后续该怎么做呢?其实我就是卡在了这个地方,看了很多讲解这个地方的博客文章,好像都没有说原因,直接拿出来一些没见过的单词,导致初次理解这个地方的我很懵。
没关系
我说说我个人理解的思路,思路很重要,建议多读几遍!
1.我们既然给obj对象的所有属性添加了get set,为了方便我们操作,我们是不是应该在get的时候,记录一下谁
使用了我这个变量。
2.在set的时候,告诉所有使用了这个变量的 谁
,你应该刷新dom了。
function defineReactive(obj, key, data) {
observe(obj[key])
// 1. 来一个数组存储,我们的数据
var arr=[]
Object.defineProperty(obj, key, {
get: function () {
// 2. 使用的时候 把这个 `谁` 保存起来 。(不知道数组存什么,别着急后面会说,暂时我们存一个对象来表示)
arr.push({
name:"谁"
})
console.log('get获取值' + key)
return data
},
set: function (newValue) {
console.log('set 设置值' + key)
data = newValue
observe(data)
// 3. 值改变的时候,通知 arr中 所有的 `谁` 去更新dom等操作
arr.forEach(item=>{
console.log(item)
})
}
})
}
先不考虑谁是什么,我们把上面代码优化一下,专业的事情专业的人做,收集依赖 交给Dep
// 1. 定义一个构造函数
function Dep() {
// 2. new 出来的实例对象要有一个属性来存放 谁使用了数据
this.subs = []
}
// 3. new出来的实例对象,有一个方法addSubs :可以收集 `谁` (这里也可以成为收集依赖)
Dep.prototype.addSubs = function (item) {
this.subs.push(item)
}
// 4. new出来的实例对象,有一个方法addSubs :可以收集 `谁` (这里也可以成为收集依赖)
Dep.prototype.notify = function (item) {
this.subs.forEach(item => {
console.log(item)
})
}
function defineReactive(obj, key, data) {
observe(obj[key])
// 1
var dep=new Dep()
Object.defineProperty(obj, key, {
get: function () {
// 2
dep.addSubs({name:'谁'})
console.log('get获取值' + key)
return data
},
set: function (newValue) {
console.log('set 设置值' + key)
data = newValue
observe(data)
// 3
dep.notify()
}
})
}
这个dep对象,它在get中收集了依赖
,在set中发布了改变信息,这个 dep 就是发布者。
现在我们收集的依赖是{name:'谁'}
。收集它肯定没有意义。那这个数据怎么去定义?。
6. 订阅者 watcher
怎么理解?首先,什么东西会用到我们的数据,仔细想想,其实本质上是页面会拿到我们的数据用来展示。所以数据来源就是我们页面。(当然我这里表达可能不是很准确,后续彻底理解虚拟dom再作更改)
// 1.定义一个构造函数
function Watcher(vm, exp, callback) {
// 2. 对象 可以理解为一个对象 var obj= {a:{b:{c:1}}}
this.vm = vm
// 3. 表达式 可以理解为一个表达式 例如 a.b.c
this.exp = exp
// 5.所以 this.vm[this.exp] 可以理解为 obj.a.b.c (当然直接这样 obj['a.b.c'] 可能无法读取,这里找个工具函数 parsePath 转换一下格式)
// 4. 回调函数
this.callback = callback
// 7.定义一个 value 存储 vm中本身的值
// this.value= parsePath(this.vm,this.exp)
// 8.但是 第7步 除了存储数据,还要有其他的事情要做,我们把它抽离到 getValue 一个方法中
this.value = this.getValue()
}
Watcher.prototype.getValue = function () {
let value = parsePath(this.vm, this.exp)
return value // 9. 切记return
}
// 10. 我们在编写dep的时候呢,我们这个对象可以做点什么。所以需要有一个 “做点什么的方法”
Watcher.prototype.updata = function () {
console.log('做点什么')
// 11. 做点什么呢?仔细想想,可以吧最新的值更新到 this.value上
this.value = parsePath(this.vm, this.exp)
// 12.其次可以执行callback函数
this.callback()
}
7.传递Watcher的实例
- 什么时候传递呢?在new Watcher()的时候传递,详情见下面的代码注释; 步骤13-17
- 我们Watcher构造函数写好了,怎么传递给到dep.addSubs() 方法中呢,可以放在全局变量上直接传递,确保唯一。
- 其次优化一下我们的callback函数,模仿vue的watch实现一下,支持两个参数,一个最新的值,一个旧值,修改一下callback的this指向为 this.vm。
var obj = {
a: 1,
b: 2,
c: 3,
d: {
m: 3
}
}
function defineReactive(obj, key, data) {
observe(obj[key])
var dep = new Dep()
Object.defineProperty(obj, key, {
get: function () {
// 15. 在get函数判断一下 如果 window.tomato存在,就存一下我们这个实例
if (window.tomato) {
dep.addSubs(window.tomato)
}
console.log('get获取值' + key)
return data
},
set: function (newValue) {
console.log('set 设置值' + key)
data = newValue
observe(data)
dep.notify()
}
})
}
function Dep() {
this.subs = []
}
Dep.prototype.addSubs = function (item) {
this.subs.push(item)
}
Dep.prototype.notify = function () {
this.subs.forEach(item => {
console.log(item)
})
}
// 1.定义一个构造函数
function Watcher(vm, exp, callback) {
console.log(this)
// 2. 对象 可以理解为一个对象 var obj= {a:{b:{c:1}}}
this.vm = vm
// 3. 表达式 可以理解为一个表达式 例如 a.b.c
this.exp = exp
// 5.所以 this.vm[this.exp] 可以理解为 obj.a.b.c (当然直接这样 obj['a.b.c'] 可能无法读取,这里找个工具函数 parsePath 转换一下格式)
// 4. 回调函数
this.callback = callback
// 7.定义一个 value 存储 vm中本身的值
// this.value= parsePath(this.vm,this.exp)
// 8.但是 第7步 除了存储数据,还要有其他的事情要做,我们把它抽离到 getValue 一个方法中
this.value = this.getValue()
}
Watcher.prototype.getValue = function () {
/*
13.
+ 首先我们给 数据所有属性 加上 get set ==》 observe(obj)
+ 当我们new Watcher的时候 就会执行构造函数Watcher中的代码。 ==》 new Watcher()
+ 执行到 this.getValue() 的时候,就会调用这个getValue函数 ==》 getValue()
+ ***然后执行到 parsePath(this.vm, this.exp) 这个时候就会读取数据 就会触发parsePath(this.vm, this.exp)的 get函数
*/
// 14. 在读取数据之前,存储当前的this 当前的this就是 Watcher的实例,存储到哪里呢?随意全局的变量下是可以的,我这里为了区分就存储在了 tomato变量下,可以换成其他变量
window.tomato = this
let value = parsePath(this.vm, this.exp)
// 16.为了防止重复塞入数据,清空全局变量 window.tomato
window.tomato = null
return value // 9. 切记return
}
// 10. 我们在编写dep的时候呢,我们这个对象可以做点什么。所以需要有一个 “做点什么的方法”
Watcher.prototype.updata = function () {
let oldValue = this.value
// 11. 做点什么呢?仔细想想,可以吧最新的值更新到 this.value 上
this.value = parsePath(this.vm, this.exp)
// 12.其次可以执行callback函数
// this.callback()
// 17.优化一下callback
callback.call(this.vm, this.value, oldValue)
}
// 6. 转换的工具函数
function parsePath(obj, expression) {
const segments = expression.split('.')
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
function observe(obj) {
if (!obj || typeof obj !== 'object') return
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
defineReactive(obj, key, obj[key])
}
}
}
observe(obj)
var wq = new Watcher(obj, 'd.m', function () {
console.log('传入一个函数')
})
obj.d.m = 333
console.log(obj.d.m)
8.完整代码
<!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>vue2 双向数据绑定</title>
</head>
<body>
</div>
<script>
var obj = {
a: 10,
b: {
c: "tomato"
}
}
function defineReactive(obj, key, data) {
observe(obj[key])
let dep = new Dep()
Object.defineProperty(obj, key, {
get: function () {
console.log('get方法执行了,读取字段:' + key,)
dep.depend()
return data
},
set: function (newValue) {
console.log('set方法执行了,设置字段:' + key)
data = newValue
dep.notify()
}
})
}
function observe(object) {
if (!obj || typeof object !== 'object') return
for (const key in object) {
if (Object.hasOwnProperty.call(object, key)) {
defineReactive(object, key, object[key])
}
}
}
function Dep() {
this.subs = []
}
Dep.prototype.addSubs = function (item) {
this.subs.push(item)
}
Dep.prototype.notify = function () {
this.subs.forEach(item => {
console.log(item)
})
}
Dep.prototype.depend = function () {
if (Dep.target) {
this.addSub(Dep.target)
}
}
function Watcher(data, exp, callback) {
this.data = data
this.exp = exp
this.callback = callback
this.value = this.getValue()
}
Watcher.prototype.getValue = function () {
Dep._target = this
let value = parsePath(this.data, this.exp)
Dep._target = null
return value
}
Watcher.prototype.update = function () {
let oldVal = this.value
this.value = parsePath(this.data, this.exp)
this.callback.call(this.data, oldVal, this.value)
}
function parsePath(obj, expression) {
const segments = expression.split('.')
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
observe(obj)
var wq = new Watcher(obj, 'b.c', function (oldVal, newVal) {
console.log('旧的', oldVal, '新的', newVal)
})
// obj.b.c=123
// console.dir(obj)
</script>
</body>
</html>
end
- 后续实际过完vue2的源码之后,再来继续完善这方面的笔记。
- 其次剩余 数组待处理,class写法待处理。
- 在这 watcher到底处理什么数据 这个有待学习。
- 最后,加油啦小番茄。
最后
以上就是可靠饼干为你收集整理的学习vue2双向数据绑定原理的全部内容,希望文章能够帮你解决学习vue2双向数据绑定原理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复