vue2的双向数据绑定原理,以及修改数据页面不更新的原理和解决方案
vue2的双向数据绑定原理,以及修改数据页面不更新的原理和解决方案
MVVM
MVVM(Model-View-ViewModel)是对 MVC(Model-View-Control)的进一步改进。
- View:视图层(template)
- ViewModel:业务逻辑层(vue实例)
- Model:数据层(data)
MVVM 将数据双向绑定(data-binding)作为核心思想,View 和 Model 之间没有联系,它们通过 ViewModel
这个桥梁进行交互
Model 和 ViewModel 之间的交互是双向的,因此 View 的变化会自动同步到 Model,而 Model的变化也会立即反映到 View 上显示
当用户操作 View,ViewModel 感知到变化,然后通知 Model 发生相应改变;反之当 Model 发生改变,ViewModel也能感知到变化,使 View 作出相应更新
双向绑定的核心: Object.defineProperty()
Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
obj:要定义属性的对象
prop:要定义或修改的属性的名称或 Symbol
descriptor:要定义或修改的属性描述符
返回值:被传递给函数的对象
js通过Object.defineProperty方法简单的实现双向绑定
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<input type="text" id="app">
<span id="childSpan"></span>
</body>
<script>
var obj = {}
var initValue='初始值'
Object.defineProperty(obj,'initValue',{
get(){
console.log('获取obj最新的值');
return initValue
},
set(newVal){
initValue = newVal
console.log('设置最新的值');
// 获取到最新的值 然后将最新的值赋值给我们的span
document.getElementById('childSpan').innerHTML = initValue
console.log(obj.initValue);
}
})
document.addEventListener('keyup', function (e) {
obj.initValue = e.target.value; //监听文本框里面的值 获取最新的值 然后赋值给obj
})
</script>
</html>
vue2响应式原理
vue2的双向数据绑定(又称响应式)原理,是通过数据劫持结合发布订阅模式的方式来实现的,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变。
官网上写追踪数据变化原理
- 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property ,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
- 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在data中的 property 被访问和修改时通知变更。
- 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
vue2双向数据绑定过程
- Observer数据劫持,递归遍历所有data属性,全部转化为getter/setter,当获取属性值的时候,触发getter,更新属性值的时候触发setter
- Compile解析模板指令,初始化视图,将模板指令转化为数据,这个时候会获取数据触发属性的getter,在getter中收集依赖,collect as Dependency,并添加订阅者Watcher,每次获取数据都会生成一个订阅者,每个订阅者自身都有一个update函数,能够更新虚拟Dom节点;
- 当修改属性值的时候,会触发setter,依赖会立马通知Notify所有订阅者调用自身的update回调函数,重新渲染虚拟DOM,更新视图
vue2双向数据绑定原理源码
class Vue {
// 参数为对象实例 这个对象用于告知vue需要挂载到哪个元素并挂载数据
constructor(obj_instance) {
// 给实例赋值对象的data属性
this.$data = obj_instance.data;
// 进行数据劫持 监听对象里属性的变化
Observer(this.$data);
Complie(obj_instance.el, this);
}
}
//模板解析 —— 替换DOM内容 把vue实例上的数据解析到页面上
// 接收两个参数 1.vue实例挂载的元素<div id="app"> 2.vue实例
function Complie(element, vm) {
vm.$el = document.querySelector(element);
// 使用文档碎片来临时存放DOM元素 减少DOM更新
const fragment = document.createDocumentFragment();
let child;
// 将页面里的子节点循环放入文档碎片
while ((child = vm.$el.firstChild)) {
fragment.appendChild(child);
}
fragment_compile(fragment);
// 替换fragment里文本节点的内容
function fragment_compile(node) {
// 使用正则表达式去匹配并替换节点里的{{}}
const pattern = /\{\{\s*(\S+)\s*\}\}/;
if (node.nodeType === 3) {
// 提前保存文本内容 否则文本在被替换一次后 后续的操作都会不生效
// 打工人: {{name}} => 打工人:西维 如果不保存后续修改name会匹配不到{{name}} 因为已经被替换
const texts = node.nodeValue;
// 获取正则表达式匹配文本字符串获得的所有结果
const result_regex = pattern.exec(node.nodeValue);
if (result_regex) {
const arr = result_regex[1].split("."); // more.salary => ['more', 'salary']
// 使用reduce归并获取属性对应的值 = vm.$data['more'] => vm.$data['more']['salary']
const value = arr.reduce((total, current) => total[current], vm.$data);
node.nodeValue = texts.replace(pattern, value);
// 在节点值替换内容时 即模板解析的时候 添加订阅者
// 在替换文档碎片内容时告诉订阅者如何更新 即告诉Watcher如何更新自己
new Watcher(vm, result_regex[1], (newVal) => {
node.nodeValue = texts.replace(pattern, newVal);
});
}
}
// 替换绑定了v-model属性的input节点的内容
if (node.nodeType === 1 && node.nodeName === "INPUT") {
const attr = Array.from(node.attributes);
attr.forEach((item) => {
if (item.nodeName === "v-model") {
const value = item.nodeValue
.split(".")
.reduce((total, current) => total[current], vm.$data);
node.value = value;
new Watcher(vm, item.nodeValue, (newVal) => {
node.value = newVal;
});
node.addEventListener("input", (e) => {
// ['more', 'salary']
const arr1 = item.nodeValue.split(".");
// ['more']
const arr2 = arr1.slice(0, arr1.length - 1);
// vm.$data.more
const final = arr2.reduce(
(total, current) => total[current],
vm.$data
);
// vm.$data.more['salary'] = e.target.value
final[arr1[arr1.length - 1]] = e.target.value;
});
}
});
}
// 对子节点的所有子节点也进行替换内容操作
node.childNodes.forEach((child) => fragment_compile(child));
}
// 操作完成后将文档碎片添加到页面
// 此时已经能将vm的数据渲染到页面上 但还未实现数据变动的及时更新
vm.$el.appendChild(fragment);
}
//数据劫持 —— 监听实例里的数据
function Observer(data_instance) {
// 递归出口
if (!data_instance || typeof data_instance !== "object") return;
// 每次数据劫持一个对象时都创建Dependency实例 用于区分哪个对象对应哪个依赖实例和收集依赖
const dependency = new Dependency();
Object.keys(data_instance).forEach((key) => {
// 使用defineProperty后属性里的值会被修改 需要提前保存属性的值
let value = data_instance[key];
// 递归劫持data里的子属性
Observer(value);
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
// 收集数据依赖
get() {
// console.log(`获取了属性值 ${value}`);
Dependency.target && dependency.addSub(Dependency.target);
return value;
},
// 触发视图更新
set(newVal) {
console.log(`修改了属性值`);
value = newVal;
// 处理赋值是对象时的情况
Observer(newVal);
dependency.notify();
},
});
});
}
//依赖 —— 实现发布-订阅模式 用于存放订阅者和通知订阅者更新
class Dependency {
constructor() {
this.subscribers = []; // 用于收集依赖data的订阅者信息
}
addSub(sub) {
this.subscribers.push(sub);
console.log(this.subscribers)
}
notify() {
console.log(this.subscribers)
this.subscribers.forEach((sub) => sub.update());
}
}
// 订阅者
class Watcher {
// 需要vue实例上的属性 以获取更新什么数据
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
//临时属性 —— 触发getter 把订阅者实例存储到Dependency实例的subscribers里面
Dependency.target = this;
key.split(".").reduce((total, current) => total[current], vm.$data);
Dependency.target = null; // 防止订阅者多次加入到依赖实例数组里
}
update() {
const value = this.key
.split(".")
.reduce((total, current) => total[current], this.vm.$data);
this.callback(value);
}
}
检测变化的注意事项
- Vue 无法检测
- property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
比如:
var vm = new Vue({
data:{
formData:{
acNo: "89111"
},
list: []
}
})
// `vm.formData.acNo` 是响应式的(当acNo变化的时候,视图有用到acNo的地方会自动更新)
vm.formData.acName = "小小"
// `vm.formData.acName` 是非响应式的(当a变化的时候,视图有用到b的地方不会自动更新)
- 对于已经创建的实例对象,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。
Vue.set(vm.formData,'acName ',"小小")
//或者
this.$set(this.formData,'acName',"小小")
-
数组
Vue 不能检测以下数组的变动:
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength
// Vue.set
this.$set(this.list, 2, "新值")
// Array.prototype.splice
this.list.splice(2, 1, "新值")
更多推荐
所有评论(0)