输入框内输入外面内容有人修改
控制台修改message内容
<div id="app">
<input type="text" v-model="message">
{{message}}
</div>
<script>
const app = new Vue({
el:'#app',
data: {
message: '哈哈',
}
})
</script>
我们不引入Vue直接写ES6代码,我们new了一个vue就要写一个vue的类
<script>
class Vue {
constructor(options) {
//1.保存数据
this.$options = options;
this.$data = options.data;
this.$el = options.el;
//2.将data添加到响应式系统中
new Observer(this.$data);
//3.代理this.$data的数据
Object.keys(this.$data).forEach(key => {
this._proxy(key)
});
//4.处理el
new Compiler(this.$el, this)
}
_proxy(key) {
Object.defineProperty(this,key,{
configurable: true,
enumerable: true,
set(newValue) {
this.$data[key] = newValue
},
get() {
return this.$data[key]
}
})
}
}
模块介绍
https://www.php.cn/js-tutorial-390322.html
Observer
遍历data中的所有属性,同时通过递归的方式遍历这些属性的子属性,通过Object.defineProperty()将这些属性都定义为访问器属性,访问器属性自带get和set方法,我们通过get和set方法来监听数据变化。
class Observer {
constructor(data) {
this.data = data;
Object.keys(data).forEach(key => {
this.defineReactive(this.data, key, data[key])
})
}
defineReactive(data, key, val) {
//一个属性Key对应一个Dep对象
const dep = new Dep();
Object.defineProperty(data, key,{
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set(newValue) {
if (newValue === val) {
return
}
val = newValue;
dep.notify()
}
})
}
}
Dep
我们需要消息订阅器来收集所有的订阅者,就好像是一个列表,当属性的get方法被触发时,需要判断是否要添加订阅者,如果需要就在列表中增加一个订阅者,当set方法被触发时就通知列表中所有的订阅者来做出响应。
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
Watcher
因为我们是在get函数中判断是否要添加订阅者的,要想把一个订阅者添加到列表中我们就需要在初始化这个订阅者时触发get函数,我们可以在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了。\
class Watcher {
constructor(node, name, vm) {
this.node = node;
this.name = name;
this.vm = vm;
Dep.target = this;
this.update();
Dep.target = null;
}
update() {
this.node.nodeValue = this.vm[this.name]
}
}
Compile
compile负责初始化时的编译解析,遍历每一个结点,看哪些结点需要订阅者,也负责后续为订阅者绑定更新函数。
const reg = /\{\{(.+)\}\}/;
// .匹配任何内容(除了特殊的符号)
// * 0或多个
// + 1或多个
// {}在正则中有特殊的含义
// regular正规 expression表达式
class Compiler {
constructor(el, vm) {
this.el = document.querySelector(el);
this.vm = vm;
this.frag = this._createFragment();
//这里我们用到的是_createFragment返回的frag,这样就不会被移除了
this.el.appendChild(this.frag)
}
_createFragment() {
const frag = document.createDocumentFragment();
let child;
//遍历每一个children结点
while(child = this.el.firstChild){
this._compile(child);
//结点放到frag里
frag.appendChild(child);
}
return frag
}
_compile(node) {
if (node.nodeType === 1) {//标签节点
const attrs = node.attributes;
if (attrs.hasOwnProperty('v-model')) {
const name = attrs['v-model'].nodeValue;
node.addEventListener('input', e => {
this.vm[name] = e.target.value;
})
}
}
if (node.nodeType === 3) {//文本节点
console.log(reg.test(node.nodeValue));
if (reg.test(node.nodeValue)) {
const name = RegExp.$1.trim();
console.log(name);
new Watcher(node, name, this.vm)
}
}
}
}
</script>
数据双向绑定原理
参考链接:https://github.com/DMQ/mvvm
数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
总的来说,就以下几个步骤:
- 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
- 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
- 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
- mvvm入口函数,整合以上三者