开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第一天,点击查看活动详情
背景
为什么会写这个文章呢?当然是因为现在处在一个特殊的环境中
双向绑定原理定义
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
数据劫持 Object.defineProperty()
我觉得vue2出了这么久大家对这个应该不陌生,在这我就简单的举个例子
Object.defineProperty()会接收三个参数,第一个参数是对象名,第二个参数是对象的属性,第三个是属性的相关操作包括属性值。 意思就是给属性增加一个监听,当去更改属性或者获取属性的时候我们都可以知道
const person = {
name: "小明",
};
function updateObj(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(val, "get获取name值");
return val;
},
set(newVal) {
console.log(newVal, "set设置name值");
val = newVal;
return val;
},
});
}
updateObj(person, "name", (val = "小明"));
核心内容搞明白了哪我们开始下一步,这里我先贴一段代码 这块代码可以解释为当我们创建了一个Vue实例时,其实就是执行Vue构造函数(这块不理解的可以看我其他文章)构造函数里面增加一个data属性,属性值是我们传进来的data,接着去调用observe方法,接收两个参数一个是data 一个是当前实例的this,接下来我们看observe方法
<div id="app">
双向绑定原理
<input type="name" v-model="name" /> {{name}}
</div>
<script type="text/javascript">
function Vue(options) {
this.data = options.data;
observe(this.data, this);
// 模版解析对应的操作
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: "app",
data: {
name: "小明",
},
});
</script>
observe
这个方法暂时只看我们想看的,就是循环我们传进来的data拿到对应的key,去调用defineReactive(vm, key, obj[key]); 第一个参数this指向实例对象、key、key的value, 这里面其实主要就使用了Object.defineProperty给属性添加get和set其他操作先不看
//实现一个观察者,对于一个实例 每一个属性值都进行观察。
function observe(obj, vm) {
for (let key of Object.keys(obj)) {
defineReactive(vm, key, obj[key]);
}
}
//实现一个响应式监听属性的函数。一旦有赋新值就发生变化
function defineReactive(obj, key, val) {
var dep = new Dep(); //观察者实例
Object.defineProperty(obj, key, {
get: function () {
if (Dep.target) {
//每一个观察着都是唯一的
dep.addSub(Dep.target);
console.log(dep, "dep");
}
return val;
},
set: function (newVal) {
if (newVal === val) {
return;
}
val = newVal;
console.log("新值" + val);
//一旦更新立马通知
dep.notify();
},
});
}
模版解析compile
nodeToFragment函数主要获取我们的节点信息每个节点调用compile方法,然后判断节点类型,如果是元素节点此处input,先获取input上绑定的属性 v-model type=‘text’ 之类的得到一个数组对象,然后就判断那个是v-model,获取到绑定的属性name然后添加input事件
if (node.nodeType === 1) {
var attr = node.attributes;
//解析元素节点的所有属性
for (let i = 0; i < attr.length; i++) {
if (attr[i].nodeName == "v-model") {
//获取v-model的值 此处是name
var name = attr[i].nodeValue; //看看是与哪一个数据相关
node.addEventListener("input", function (e) {
//将与其相关的数据改为最新值
vm[name] = e.target.value;
});
node.value = vm.data[name]; //将data中的值赋予给该node
node.removeAttribute("v-model");
}
}
}
如果是文本节点 就判断是不是模版{{}} 如果是获取到里面绑定属性,然后node.nodeValue = vm[name]; 然后获取实例上对应属性的value赋值给node,
//如果是文本节点
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; //获取到匹配的字符串
name = name.trim();
node.nodeValue = vm[name]; //将data中的值赋予给该node
new Watcher(vm, node, name); //绑定一个订阅者
}
}
注意此时已经触发了该属性绑定的get方法,由于里面加了if (Dep.target) 判断所以只返回了value,没做其他操作
var dep = new Dep(); //观察者实例
Object.defineProperty(obj, key, {
get: function () {
if (Dep.target) {
//每一个观察着都是唯一的
dep.addSub(Dep.target);
console.log(dep, "dep");
}
return val;
},
set: function (newVal) {
if (newVal === val) {
return;
}
val = newVal;
console.log("新值" + val);
//一旦更新立马通知
dep.notify();
},
});
接着到到 new Watcher(vm, node, name); 此处我认为是在进行依赖收集,在Dep构造函数上增加Dep.target = this,this指向当前watcher实例,然后就是一些初始化属性的操作,
接着调用自身update方法,然后执行get方法获取当前属性的value, this.value = this.vm[this.name];注意此时又触发了此属性的get 此时if (Dep.target)成立进入内部 把Dep.target放进sub数组中,接着将获取到的值赋值,然后Dep.target=null,防止重复添加
function Watcher(vm, node, name) {
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update() {
this.get();
this.node.nodeValue = this.value; //更改节点内容的关键
},
get() {
this.value = this.vm[this.name]; //触发相应的get
},
};
此时其实整个的双向绑定原理就结束了,当我们改变name的时候触发set,如果值相等则直接return不相等在则去调用 dep.notify();,循环当前节点所有的依赖(Watcher实例) 执行实例自身update方法
set: function (newVal) {
if (newVal === val) {
return;
}
val = newVal;
console.log("新值" + val);
//一旦更新立马通知
dep.notify();
},
End
这样写个文章理解下来面试的时候被问到就不慌了,之前面试官问我原理我只会说出文章开头双向绑定原理的定义,这次我可以多说点了也不知道有没有用。