网站首页 > 技术文章 正文
v-model原理
如果觉得看文章不过瘾,可以直接观看视频版(建议先看文章,再看视频):
注意视频和文章稍稍有一些区别:
- 视频中是用es6的class类的方式创建对象的
- 视频中`初始化data数据`时没有做递归处理,此处应该以文章为准
- 视频中获取data数据没有使用reduce循环,此处应该以文章为准
目标
1- 前置知识
1. Object.defineProperty
<script>
var Person={}
var reactiveName = "zhangsan"
Object.defineProperty(Person, 'name', {
// 可配置
configurable: true,
// 可枚举
enumerable: true,
// 访问器属性`get`
// 读取`Person.name`的时候触发的方法
get () {
console.log('执行get')
return reactiveName
},
// 访问器属性`set`
// 修改`Person.name`的时候触发的方法
set (newValue) {
console.log('执行set')
reactiveName = newValue
}
})
console.log(Person.name); // 'zhangsan'
Person.name = 'lisi';
console.log(Person.name); // 'lisi'
</script>
2. 观察者模式
<script>
// 订阅者
class Dep {
constructor() {
this.watchers = []
}
// 添加观察者
addWatcher (watcher) {
this.watchers.push(watcher)
}
// 通知
notify () {
this.watchers.forEach(watcher => {
watcher.update()
})
}
}
// 观察者
class Watcher {
constructor(callback) {
this.callback = callback
}
update () {
console.log('update');
this.callback();
}
}
// 创建订阅者
const dep = new Dep();
// 创建观察者
const watcher1 = new Watcher(() => console.log('watcher1'));
const watcher2 = new Watcher(() => console.log('watcher2'));
// 添加观察者
dep.addWatcher(watcher1)
dep.addWatcher(watcher2)
// 触发通知
dep.notify();
</script>
3. 小结
2- 实现v-model
1. 采用类似Vue的调用方式
<div id="app">
<input type="text" v-model="msg" />
</div>
<script>
const vm = new MyVue({
el: '#app',
data: {
msg: 'abc'
}
})
</script>
所以我们要创建MyVue对象
<script>
class MyVue {
constructor({el, data}) {
this.container = document.querySelector(el);
this.data = data;
}
}
</script>
2. 初始化data数据
<script>
class MyVue {
constructor({el, data}) {
this.container = document.querySelector(el);
this.data = data;
// 初始化data
this.initData(this, this.data)
}
initData(vm, data) {
for (let key in data) {
let initVal = data[key];
Object.defineProperty(vm, key, {
get() {
return initVal
},
set(val) {
initVal = val;
}
})
// 判断是否是对象,递归调用initData
if (Object.prototype.toString.call(data[key]) === "[object Object]") {
this.initData(vm[key], data[key])
}
}
}
}
</script>
<script>
const vm = new MyVue({
el: '#app',
data: {
msg: 'abc'
}
});
console.log(vm.msg) // abc
</script>
3. 初始化v-model表单
<script>
class MyVue {
constructor({ el, data }) {
this.container = document.querySelector(el);
this.data = data;
// 初始化data
this.initData(this, this.data)
// 初始化v-model表单
this.initVModel();
}
initVModel () {
// 获取所有v-model元素
const nodes = this.container.querySelectorAll('[v-model]');
nodes.forEach(node => {
const key = node.getAttribute('v-model');
// 初始化赋值
node.value = this[key];
// 监听输入事件
node.addEventListener('input', ev => {
this[key] = ev.target.value;
}, false)
});
}
}
</script>
上面的代码只能拿到data中第一层的数据,如果是这样绑定:
<input type="text" v-model="obj.a" />
那其实获取 this['obj.a']是获取不到数据的,所以还需要重写一个获取data数据的方法:
getData(str) {
const arr = str.split('.'); // ["obj", "a"]
const res = arr.reduce((target, item) => {
// 第一次遍历 target等于data
return target[item] // return之后,target就等于target[item] ===> data.obj
// 第二次遍历 target等于data.obj
// return之后,target就等于data.obj.a
}, this)
return res;
},
initVModel () {
// 获取所有v-model元素
const nodes = this.container.querySelectorAll('[v-model]');
nodes.forEach(node => {
const key = node.getAttribute('v-model');
// 初始化赋值
// node.value = this[key]; // 如果key是"obj.a"这种字符串,获取不到数据
node.value = this.getData(key);
// 监听输入事件
node.addEventListener('input', ev => {
// this[key] = ev.target.value; // 也要处理key是"obj.a"格式的字符串
const arr = key.split("."); // ["obj", "a"]
// 如果arr只有一个元素,直接赋值
if (arr.length === 1) {
this[key] = node.value;
return;
}
// 获取倒数第二级的对象(对于`obj.a`,那就获取this.obj)
const res = this.getData(key.substring(0, key.lastIndexOf("."))) // this.obj
// 赋值最后一级(this.obj.a = node.value)
res[arr[arr.length - 1]] = node.value;
}, false)
});
},
此时已经实现了视图变化触发模型的变化,接下来实现模型的变化触发视图的变化
4. 模型的变化触发视图更新
1. 引入观察者模式
// 订阅者
class Dep {
constructor() {
this.watchers = []
}
// 添加观察者
addWatcher (watcher) {
this.watchers.push(watcher)
}
// 通知
notify () {
this.watchers.forEach(watcher => {
watcher.update()
})
}
}
// 观察者
class Watcher {
constructor(callback) {
this.callback = callback
}
update () {
console.log('update');
this.callback();
}
}
2. 通知触发的时机
模型驱动视图更新,就是当修改 vm.msg时(比如 vm.msg="123"),需要触发dom更新。所以我们需要在set方法里面调用 dep.notify()
// 初始化data
initData(vm, data) {
for (let key in data) {
let initVal = data[key];
//++++++++++++++++++++++++++++++
const dep = new Dep();
//++++++++++++++++++++++++++++++
Object.defineProperty(vm, key, {
get() {
return initVal
},
set(val) {
initVal = val;
//++++++++++++++++++++++++++++++
// 通知视图更新
dep.notify();
//++++++++++++++++++++++++++++++
}
})
// 判断是否是对象,递归调用initData
if (Object.prototype.toString.call(data[key]) === "[object Object]") {
this.initData(vm[key], data[key])
}
}
}
3. 创建观察者
观察者需要更新dom,所以需要在初始化v-model表单的时候创建观察者
initVModel () {
// 获取所有v-model元素
const nodes = this.container.querySelectorAll('[v-model]');
nodes.forEach(node => {
const key = node.getAttribute('v-model');
// 初始化赋值
// node.value = this[key]; // 如果key是"obj.a"这种字符串,获取不到数据
node.value = this.getData(key);
//++++++++++++++++++++++++++++++
// 创建观察者
const watcher = new Watcher(() => {
node.value = this.getData(key)
})
//++++++++++++++++++++++++++++++
// 监听输入事件
node.addEventListener('input', ev => {
// this[key] = ev.target.value; // 也要处理key是"obj.a"格式的字符串
const arr = key.split("."); // ["obj", "a"]
// 如果arr只有一个元素,直接赋值
if (arr.length === 1) {
this[key] = node.value;
return;
}
// 获取倒数第二级的对象(对于`obj.a`,那就获取this.obj)
const res = this.getData(key.substring(0, key.lastIndexOf("."))) // this.obj
// 赋值最后一级(this.obj.a = node.value)
res[arr[arr.length - 1]] = node.value;
}, false)
});
},
4. 添加观察者
当创建了观察者后,应该马上添加到订阅者中,但是订阅者怎么知道已经创建了观察者呢?
// 创建观察者时传入this和key
const watcher = new Watcher(this, key, () => {
node.value = this.getData(key)
})
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.callback = callback;
// 获取data数据,触发get方法
vm.getData(key);
}
update () {
console.log('update');
this.callback();
}
}
// get方法中添加观察者
get() {
dep.addWatcher(watcher)
return initVal
},
5. 将观察者实例赋值给Dep的某个属性
上面 dep.addWatcher(watcher)中的 watcher并不存在,我们需要在观察者的构造函数中先将观察者实例赋值给Dep的某个属性,再获取data数据
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.callback = callback;
//将观察者实例赋值给Dep的target属性
Dep.target = this;
// 获取data数据,触发get方法
vm.getData(key);
}
update () {
console.log('update');
this.callback();
}
}
// get方法
get() {
dep.addWatcher(Dep.target)
return initVal
},
6. 避免反复添加观察者
上面已经实现了模型数据驱动视图更新,但是我们发现update方法会执行多次,那是因为 dep.addWatcher(Dep.target)执行了多次,所以dep的watchers数组中有多个观察者,所以update方法会执行多次。解决方法就是获取data数据后重置Dep.target。
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.callback = callback;
//将观察者实例赋值给Dep的target属性
Dep.target = this;
// 获取data数据,触发get方法
vm.getData(key);
// 获取data数据后重置Dep.target,避免重复添加观察者
Dep.target = null;
}
update () {
console.log('update');
this.callback();
}
}
// get方法
get() {
Dep.target && dep.addWatcher(Dep.target)
return initVal
},
7. data多次设置为同样的值时不需要触发更新
set(val) {
// 如果前后两次设置的值相等,就不触发更新
if (initVal === val) {
return;
}
initVal = val;
dep.notify(); // 通知视图更新
}
完整代码
// 订阅者
class Dep {
constructor() {
this.watchers = []
}
// 添加观察者
addWatcher(watcher) {
this.watchers.push(watcher)
}
// 通知
notify() {
this.watchers.forEach(watcher => {
watcher.update()
})
}
}
// 观察者
class Watcher {
constructor(vm, key, callback) {
this.callback = callback
// 第二件事,把当前实例传给get方法
Dep.target = this;
// 第一件事,获取当前的data
const val = vm.getData(key)
// 为了防止重复添加观察者,添加完之后马上清空
Dep.target = null;
}
update() {
console.log('update');
this.callback();
}
}
class MyVue {
constructor({
el,
data
}) {
this.container = document.querySelector(el);
this.data = data;
// 初始化数据
this.initData(this, this.data)
// 初始化v-model表单
this.initVModel()
}
initData(vm, data) {
for (let key in data) {
let initVal = data[key];
const dep = new Dep();
Object.defineProperty(vm, key, {
get() {
// 只要获取data数据,就会进入get方法,然后添加观察者
Dep.target && dep.addWatcher(Dep.target)
return initVal
},
set(val) {
// 如果前后两次设置的值相等,就不触发更新
if (initVal === val) {
return;
}
// console.log(key + '被修改数据')
initVal = val;
dep.notify(); // 通知视图更新
}
})
// 判断是否是对象,递归调用initData
if (Object.prototype.toString.call(data[key]) === "[object Object]") {
this.initData(vm[key], data[key])
}
}
}
initVModel() {
// 获取所有的v-model表单
const nodes = this.container.querySelectorAll('[v-model]')
// console.log(nodes)
nodes.forEach(node => {
// 获取v-model属性的值
const key = node.getAttribute('v-model')
// 把表单创建成观察者
new Watcher(this, key, () => {
node.value = this.getData(key)
})
// console.log(123, key, this[key])
const val = this.getData(key)
// console.log(234234, val)
// 给表单赋值
node.value = val
// 视图驱动数据更新:监听表单事件,修改data。
node.addEventListener('input', () => {
// this[key] = node.value // key === "obj.b"
const arr = key.split("."); // ["obj", "b"]
// 如果arr只有一个元素,直接赋值
if (arr.length === 1) {
this[key] = node.value;
return;
}
// let res = this.getData(key) // this.obj.b ===> 2
const res = this.getData(key.substring(0, key.lastIndexOf("."))) // this.obj
// res = node.value;
res[arr[arr.length - 1]] = node.value;
})
})
}
// 传入'a.b.c'这种格式的字符串,获取this.a.b.c的值
getData(str) {
const arr = str.split('.'); // ["obj", "a"]
const res = arr.reduce((target, item) => {
// 第一次遍历 target等于data
return target[item] // return之后,target就等于target[item] ===> data.obj
// 第二次遍历 target等于data.obj
// return之后,target就等于data.obj.a
}, this)
return res;
}
}
总结
猜你喜欢
- 2024-10-12 学习Vue的数据绑定语法,实现动态的用户界面(四)
- 2024-10-12 Vue前端框架:绑定单个复选框。 vue复选框默认选中
- 2024-10-12 vue2响应式原理 vue2.0响应式原理
- 2024-10-12 vue数据绑定语法 vue样式绑定语法
- 2024-10-12 【Vue】第5章 Vue数据绑定和计算属性详解
- 2024-10-12 Vue数据及事件绑定、filter过滤器
- 2024-10-12 Vue开发:组件绑定自定义事件及其应用
- 2024-10-12 vue.js实战之表单绑定-单选按钮 vue表格单选
- 2024-10-12 跟神一起学前端-初识vue、数据绑定
- 2024-10-12 vue.js 动态绑定class的几种方式 vue动态绑定类
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)