Vue通过Object.defineProperty来实现监听数据的改变和读取(属性中的getter和setter方法) 实现数据劫持。下面简单记录一下,vue监听数据变化的原理
let obj = {
name:'lnj'
};
class Observer{
// 只要将需要监听的那个对象传递给Observer这个类
// 这个类就可以快速的给传入的对象的所有属性都添加get/set方法
constructor(data){
this.observer(data)
}
observer(obj){
if(obj && typeof obj === 'object'){
// 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
for(let key in obj){
this.defineRecative(obj, key, obj[key])
}
}
}
defineRecative(obj, attr, value){
this.observer(value);
Object.defineProperty(obj, attr, {
get(){
return value;
},
set: (newValue)=>{
if(value !== newValue){
this.observer(newValue);
value = newValue;
console.log('监听到数据的变化');
}
}
})
}
}
new Observer(obj);
obj.name.a = 'data';class Nue {
constructor(options){
// 1.保存创建时候传递过来的数据
if(this.isElement(options.el)){
this.$el = options.el;
}else{
this.$el = document.querySelector(options.el);
}
this.$data = options.data;
// 2.根据指定的区域和数据去编译渲染界面
if(this.$el){
new Compiler(this)
}
}
// 判断是否是一个元素
isElement(node){
return node.nodeType === 1;
}
}
class Compiler {
constructor(vm){
this.vm = vm;
}
}创建一个Compiler类用于编译指令数据
class Compiler {
constructor(vm){
this.vm = vm;
// 1.将网页上的元素放到内存中
let fragment = this.node2fragment(this.vm.$el);
console.log(fragment);
// 2.利用指定的数据编译内存中的元素
// 3.将编译好的内容重新渲染会网页上
}
node2fragment(app){
// 1.创建一个空的文档碎片对象
let fragment = document.createDocumentFragment();
// 2.编译循环取到每一个元素
let node = app.firstChild;
while (node){
// 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存储了所有元素的文档碎片对象
return fragment;
}
}在Compiler中,实现buildTemplate(处理模板)、buildElement(处理元素)、buildText(处理文本)这几个方法,对应处理指令和模板
class Compiler {
constructor(vm){
this.vm = vm;
// 1.将网页上的元素放到内存中
let fragment = this.node2fragment(this.vm.$el);
// 2.利用指定的数据编译内存中的元素
this.buildTemplate(fragment);
// 3.将编译好的内容重新渲染会网页上
}
node2fragment(app){
// 1.创建一个空的文档碎片对象
let fragment = document.createDocumentFragment();
// 2.编译循环取到每一个元素
let node = app.firstChild;
while (node){
// 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存储了所有元素的文档碎片对象
return fragment;
}
buildTemplate(fragment){
let nodeList = [...fragment.childNodes];
nodeList.forEach(node=>{
// 需要判断当前遍历到的节点是一个元素还是一个文本
// 如果是一个元素, 我们需要判断有没有v-model属性
// 如果是一个文本, 我们需要判断有没有{{}}的内容
if(this.vm.isElement(node)){
// 是一个元素
this.buildElement(node);
// 处理子元素(处理后代)
this.buildTemplate(node);
}else{
// 不是一个元素
this.buildText(node);
}
})
}
buildElement(node){
let attrs = [...node.attributes];
attrs.forEach(attr => {
let {name, value} = attr;
if(name.startsWith('v-')){
console.log('是Vue的指令, 需要我们处理', name);
}
})
}
buildText(node){
let content = node.textContent;
let reg = /\{\{.+?\}\}/gi;
if(reg.test(content)){
console.log('是{{}}的文本, 需要我们处理', content);
}
}
}定义一个CompilerUtil对象,对象的key,即是指令和数据的编译方法
let CompilerUtil = {
getValue(vm, value){
return value.split('.').reduce((data, currentKey) => {
return data[currentKey];
}, vm.$data);
},
model: function (node, value, vm) {
let val = this.getValue(vm, value);
node.value = val;
},
html: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerHTML = val;
},
text: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerText = val;
}
}let CompilerUtil = {
getValue(vm, value){
return value.split('.').reduce((data, currentKey) => {
return data[currentKey.trim()];
}, vm.$data);
},
getContent(vm, value){
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
return this.getValue(vm, args[1]);
});
// console.log(val);
return val;
},
.......
content: function (node, value, vm) {
let val = this.getContent(vm, value);
node.textContent = val;
}
}class Observer{
// 只要将需要监听的那个对象传递给Observer这个类
// 这个类就可以快速的给传入的对象的所有属性都添加get/set方法
constructor(data){
this.observer(data)
}
observer(obj){
if(obj && typeof obj === 'object'){
// 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
for(let key in obj){
this.defineRecative(obj, key, obj[key])
}
}
}
defineRecative(obj, attr, value){
this.observer(value);
Object.defineProperty(obj, attr, {
get(){
return value;
},
set: (newValue)=>{
if(value !== newValue){
this.observer(newValue);
value = newValue;
console.log('监听到数据的变化');
}
}
})
}
}// 想要实现数据变化之后更新UI界面, 我们可以使用发布订阅模式来实现
// 先定义一个观察者类, 再定义一个发布订阅类, 然后再通过发布订阅的类来管理观察者类
class Dep {
constructor(){
// 这个数组就是专门用于管理某个属性所有的观察者对象的
this.subs = [];
}
// 订阅观察的方法
addSub(watcher){
this.subs.push(watcher);
}
// 发布订阅的方法
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}
class Watcher {
constructor(vm, attr, cb){
this.vm = vm;
this.attr = attr;
this.cb = cb;
// 在创建观察者对象的时候就去获取当前的旧值
this.oldValue = this.getOldValue();
}
getOldValue(){
Dep.target = this;
let oldValue = CompilerUtil.getValue(this.vm, this.attr);
Dep.target = null;
return oldValue;
}
// 定义一个更新的方法, 用于判断新值和旧值是否相同
update(){
let newValue = CompilerUtil.getValue(this.vm, this.attr);
if(this.oldValue !== newValue){
this.cb(newValue, this.oldValue);
}
}
}let CompilerUtil = {
getValue(vm,value){
return value.split('.').reduce((data, currentKey) =>{
return data[currentKey.trim()];
},vm.$data)
},
getContent(vm,value){
let reg = /\{\{(.+?)\}\}/gi;// 提取大括号内容用 ()
let val = value.replace(reg,(...args) =>{
return this.getValue(vm,args[1]);
});
return val
},
setValue(vm, attr, newValue){
attr.split('.').reduce((data, currentAttr, index, arr)=>{
if(index === arr.length - 1){
data[currentAttr] = newValue;
}
return data[currentAttr];
}, vm.$data)
},
model:function (node,value,vm) {
new Watcher(vm, value, (newValue, oldValue)=>{
node.value = newValue;
// debugger;
});
let val = this.getValue(vm,value);
node.value = val;
node.addEventListener('input', (e)=>{
let newValue = e.target.value;
this.setValue(vm, value, newValue);
})
},
html:function (node,value,vm) {
let val = this.getValue(vm,value);
node.innerHTML = val;
},
text:function (node,value,vm) {
let val = this.getValue(vm,value);
node.innerText = val;
},
content(node,value,vm){
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args)=>{
new Watcher(vm, args[1], (newValue, oldValue)=>{
node.textContent = this.getContent(vm, value);
});
return this.getValue(vm, args[1]);
})
node.textContent = val;
},
on(node, value, vm, type){
node.addEventListener(type, (e)=>{
vm.$methods[value].call(vm,e)
})
}
};
class Nue {
constructor(option){
// 1.保存创建时候传递过来的数据
if (this.isElement(option.el)){
this.$el = option.el;
} else {
this.$el = document.querySelector(option.el)
}
this.$data = option.data;
this.proxyData();
this.$methods = option.methods;
this.$computed = option.computed;
// 将computed中的方法添加到$data中,
// 只有这样将来我们在渲染的时候才能从$data中获取到computed中定义的计算属性
this.computed2data();
// 2.根据指定的区域和数据去编译渲染界面
if (this.$el){
new Observer(this.$data);
new Compiler(this)
}
}
computed2data(){
for (let key in this.$computed){
Object.defineProperty(this.$data, key, {
get: ()=>{
return this.$computed[key].call(this)
}
})
}
}
// 判断是否是一个元素
isElement(node){
return node.nodeType === 1
}
// 实现数据代理, 将$data上的数据添加到Vue实例上, 这样将来才能通过this.xxx直接获取数据
proxyData(){
for (let key in this.$data){
Object.defineProperty(this, key,{
get: () =>{
return this.$data[key]
}
})
}
}
}
// 渲染方法
class Compiler {
constructor(vm){
this.vm = vm
// 1.将网页上的元素放到内存中
let fragment = this.node2fragment(this.vm.$el);
// 2.利用指定的数据编译内存中的元素
this.buildTemplate(fragment);
// 3.将编译好的内容重新渲染会网页上
this.vm.$el.appendChild(fragment);
}
node2fragment(app){
let fragment = document.createDocumentFragment();
let node = app.firstChild;
while (node){
fragment.appendChild(node);
node = app.firstChild
}
return fragment
}
buildTemplate(fragment){
let nodeList = [...fragment.childNodes];
nodeList.forEach(node=>{
// 需要判断当前遍历到的节点是一个元素还是一个文本
// 如果是一个元素, 我们需要判断有没有v-model属性
// 如果是一个文本, 我们需要判断有没有{{}}的内容
if (this.vm.isElement(node)){
// 是一个元素
this.buildElement(node);
// 处理子元素(处理后代)
this.buildTemplate(node)
} else {
this.buildText(node)
}
})
}
buildElement(node){
let attrs = [...node.attributes];
attrs.forEach(attr =>{
let { name, value } = attr;
if (name.startsWith('@')){
let [_, directiveType] = name.split('@');
CompilerUtil['on'](node, value, this.vm, directiveType)
node.removeAttribute(name);
}
if (name.startsWith('v-')){
let [directiveName, directiveType] = name.split(':');
let [_, directive] = directiveName.split('-');
CompilerUtil[directive](node, value, this.vm,directiveType);
node.removeAttribute(name); // 把自定义属性去掉
}
})
}
buildText(node){
let content = node.textContent;
let reg = /\{\{.+?\}\}/gi;
if (reg.test(content)){
CompilerUtil['content'](node, content, this.vm);
}
}
}
class Observer{
constructor(data){
this.observer(data)
}
observer(obj){
if (obj && typeof obj === 'object'){
for (let key in obj){
this.defineRecative(obj,key,obj[key]) // 递归取出所有的key
}
}
}
defineRecative(obj, attr, value){
this.observer(value);
let dep = new Dep();
Object.defineProperty(obj, attr, {
get() {
Dep.target && dep.addSub(Dep.target);
return value;
},
set:(newValue) =>{
if (value !== newValue){
this.observer(newValue);
value = newValue;
dep.notify();
console.log('需要更新');
}
}
})
}
}
// 想要实现数据变化之后更新UI界面, 我们可以使用发布订阅模式来实现
// 先定义一个观察者类, 再定义一个发布订阅类, 然后再通过发布订阅的类来管理观察者类
class Dep {
constructor(){
this.subs = []
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher => watcher.update());
}
}
class Watcher {
constructor(vm, attr, cb){
this.vm = vm;
this.attr = attr;
this.cb = cb;
this.oldValue = this.getOldValue()
}
getOldValue(){
Dep.target = this;
let oldValue = CompilerUtil.getValue(this.vm, this.attr);
Dep.target = null;
return oldValue;
}
update(){
let newValue = CompilerUtil.getValue(this.vm, this.attr);
if(this.oldValue !== newValue){
this.cb(newValue, this.oldValue);
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>01-Vue基本模板</title>
<!--1.下载导入Vue.js-->
<!-- <script src="js/vue.js"></script>-->
<script src="js/nue.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="name">
<input type="text" v-model="time.h">
<input type="text" v-model="time.m">
<input type="text" v-model="time.s">
<div v-html="html">abc</div>
<div v-text="text">123</div>
<p>{{ name }}</p>
<p>{{age}}</p>
<p>{{time.h}}</p>
<p>{{name}}-{{age}}</p>
<ul>
<li>6</li>
<li>6</li>
<li>6</li>
</ul>
</div>
<script>
// 2.创建一个Vue的实例对象
// let vue = new Vue({
let vue = new Nue({
// 3.告诉Vue的实例对象, 将来需要控制界面上的哪个区域
el: '#app',
// el: document.querySelector('#app'),
// 4.告诉Vue的实例对象, 被控制区域的数据是什么
data: {
name: "lnj",
age: 33,
time: {
h: 11,
m: 12,
s: 13
},
html: `<div>我是div</div>`,
text: `<div>我是div</div>`
}
});
</script>
</body>
</html>