Vue响应式原理
Vue
响应式的原理(数据改变界面就会改变),实时监听数据变化,一旦数据发生变化就更新界面。Vue
是通过原生JS的defineProperty
方法来如何实现实时监听数据变化。
defineProperty
// 可以直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
Object.defineProperty(obj, prop, descriptor)
obj: 需要操作的对象
prop: 需要操作的属性
descriptor: 属性描述符
let obj = {name: 'meihao'};
// 给obj对象动态新增一个name属性, 并且name属性的取值必须是test
Object.defineProperty(obj, 'name', {
// 可以通过value来告诉defineProperty方法新增的属性的取值是什么
value: 'test',
// 默认情况下通过defineProperty新增的属性的取值是不能修改的
// 如果想修改, 那么就必须显示的告诉defineProperty方法
writable: true,
// 默认情况下通过defineProperty新增的属性是不能删除的
// 如果想删除, 那么就必须显示的告诉defineProperty方法
configurable: true,
// 默认情况下通过defineProperty新增的属性是不能迭代的
// 如果想迭代遍历, 那么就必须显示的告诉defineProperty方法
enumerable: true
});
obj.name = 'test02'; // 修改
delete obj.name // 删除
for(let key in obj){ // 遍历
console.log(key, obj[key]);
}
get/set方法
defineProperty
除了可以动态修改/新增
对象的属性以外,还可以在修改/新增的时候给该属性添加get/set
方法。
通过defineProperty
给某个属性添加了get/set
方法,那么以后只要获取这个属性的值就会自动调用get
, 设置这个属性的值就会自动调用set
。
如果设置了get/set
方法, 那么就不能通过value
直接赋值, 也不能编写writable:true
。
let obj = {};
let oldValue = 'mei';
Object.defineProperty(obj, 'name', {
// value: oldValue,
// writable: true,
configurable: true,
enumerable: true,
get(){
console.log("get方法被执行了");
return oldValue;
},
set(newValue){
if(oldValue !== newValue){
console.log("set方法被执行了");
oldValue = newValue;
}
}
});
console.log(obj.name);
/*
get方法被执行了
mei
*/
obj.name = 'test';
/*
set方法被执行了
*/
监听对象取值变化
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); // 如果属性的取值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
Object.defineProperty(obj, attr, {
get(){
return value;
},
set:(newValue)=>{
if(value !== newValue) {
// 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
this.observer(newValue);
value = newValue;
console.log('监听到数据的变化');
}
}
})
}
}
// 测试
let obj = { name: 'zs' };
new Observer(obj);
obj.name = {a: 'abc'}; // 监听到数据的变化
obj.name.a = 'test'; // 监听到数据的变化
构建Vue实例
👉 要想使用Vue必须先创建Vue的实例, 创建Vue的实例通过new来创建, 所以说明Vue是一个类。所以我们要想使用自己的Vue,就必须定义一个名称叫做Vue的类
👉 只要创建好了Vue的实例, Vue就会根据指定的区域和数据, 去编译渲染这个区域。所以我们需要在自己编写的Vue实例中拿到数据和控制区域, 去编译渲染这个区域。
注意点: 创建Vue实例的时候指定的控制区域可以是一个ID名称, 也可以是一个Dom元素
注意点: Vue实例会将传递的控制区域和数据都绑定到创建出来的实例对象上 $el/$data
// nue.js
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;
}
}
// index.html
<div id="app">
<input type="text" v-model="name">
<p>{{ name }}</p>
<p>{{age}}</p>
<ul>
<li>6</li>
<li>6</li>
<li>6</li>
</ul>
</div>
let vue = new Nue({
el: '#app',
data: { name: "meihao", age: 18 }
});
console.log(vue.$el); // 拿到了控制的dom元素
console.log(vue.$data); // 拿到了Nue对象中绑定的数据
提取元素到内存
实现Compiler
,将Nue
对象传入的元素存放到内存中,需要用的js方法:createDocumentFragment
提取到内存中的对象:fragment
实现CompilerUtil
对象存放编译指令方法,index.html做如下修改
// index.html
<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>
<ul>
<li>6</li>
<li>6</li>
<li>6</li>
</ul>
</div>
Nue对象:
// index.html
let vue = new Nue({
el: '#app',
data: {
name: "meihao",
age: 11,
time: {
h: 11,
m: 12,
s: 13
},
html: `<div>我是div1</div>`,
text: `<div>我是div2</div>`
}
});
查找指令和模板
提取到内存中的文档对象,[...fragment.childNodes]
得到的是:
nodeList
编译指令数据
增加CompilerUtil工具对象,提供对应的方法获取编译指令中对应的数据,替换编译模板
监听数据变化
增加Observer类
数据驱动界面改变
实现数据变化之后更新UI界面, 需要使用发布订阅模式来实现。先定义一个观察者类Watcher, 再定义一个发布订阅类Dep, 然后再通过发布订阅的类来管理观察者类
界面驱动数据改变
Object.defineProperty监听vue对象中data的数据,定义观察者类Dep,defineProperty监听对象的get/set对象中,定义dep对象,获取data中的值的时候调用dep对象中的订阅方法,set的时候执行dep方法中的通知发布方法,执行更新操作update。
// defineProperty方法监听对象某个属性,需要创建属于当前属性的发布订阅对象,要注意this指针的指向
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);
}
}
}
实现事件指令
// index.html
<div id="app">
<input type="text" v-model="name">
<div v-on:click="myFn">我是div</div>
</div>
let vue = new Nue({
...
methods:{
myFn(){
alert('myFn被执行了');
console.log(this);
}
}
});
实现数据拦截
目的是将我们创建Nue对象的时候data中的数据绑定到Nue对象上,这样在html文件中才能通过this访问到。
class Nue {
...
// 实现数据代理, 将$data上的数据添加到Vue实例上, 这样将来才能通过this.xxx直接获取数据
proxyData(){
for(let key in this.$data){
Object.defineProperty(this, key, {
get: () => {
return this.$data[key]
}
})
}
}
}
实现计算属性
<div id="app">
<p>{{getName}}</p>
</div>
let vue = new Nue({
...
computed:{
getName(){
return this.name + '666'; // name是data中的属性
}
}
});
class Nue {
constructor(options){
if(this.isElement(options.el)){
this.$el = options.el;
}else{
this.$el = document.querySelector(options.el);
}
this.$data = options.data;
this.proxyData();
this.$methods = options.methods;
this.$computed = options.computed;
// 将computed中的方法添加到$data中,
// 只有这样将来我们在渲染的时候才能从$data中获取到computed中定义的计算属性
this.computed2data();
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)
}
})
}
}
}
完整代码
// nue.js
let CompilerUtil = {
getValue(vm, value){ // 针对Nue中data是对象的情况value=time.h
// time.h --> [time, h]
return value.split('.').reduce((data, currentKey) => {
// 第一次执行: data=$data, currentKey=time
// 第二次执行: data=time, currentKey=h
return data[currentKey];
}, vm.$data);
},
getContent(vm, value){
// console.log(value); // {{name}}-{{age}} -> meihao-18
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => { // ...args 拿到的是匹配到的值
return this.getValue(vm, args[1]); // meihao, 18
});
return val;
},
setValue(vm, attr, newValue){ // 针对model指令的一个工具方法
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) { // value=time.h
// 在第一次渲染的时候, 就给所有的属性添加观察者
new Watcher(vm, value, (newValue, oldValue)=>{
node.value = newValue; // 发布订阅,执行回调函数就会重新赋最新的值
});
/*node.value = vm.$data[value]; // vm.$data[time.h] --> vm.$data[time] --> time[h]*/
let val = this.getValue(vm, value);
node.value = val; // 最终,Nue data中的数据被渲染到dom中
node.addEventListener('input', (e)=>{ // 双向数据绑定,输入修改数据后出发
let newValue = e.target.value;
this.setValue(vm, value, newValue); // 针对对象多层级,要遍历找到最终那一级才赋值
})
},
html: function (node, value, vm) {
new Watcher(vm, value, (newValue, oldValue)=>{
node.innerHTML = newValue;
});
let val = this.getValue(vm, value);
node.innerHTML = val;
},
text: function (node, value, vm) {
new Watcher(vm, value, (newValue, oldValue)=>{
node.innerText = newValue;
});
let val = this.getValue(vm, value);
node.innerText = val;
},
content: function (node, value, vm) { // 这个方法拿到的是标签里的模板内容,需要替换对应的值
// console.log(value); // value 就是匹配到的模板{{ name }} -> name -> $data[name]
// let val = this.getContent(vm, value);
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args)=>{ // 外层是为了拿到属性名称
/*
value = {{ name }}, ..args 拿到的内容是:["{{ name }}", " name ", 0, "{{ name }}"]
{{name}}-{{age}} --> meihao-18 原始内容
newValue -> test --> 修改的newValue是test
meihao-18 -> test
meihao-18 -> test-18
* */
// 内层是为了保证数据完整性
new Watcher(vm, args[1], (newValue, oldValue)=>{
node.textContent = this.getContent(vm, value); // 一个标签可能有多个模板{{name}}-{{age}},所有要通过getContent方法完整替换一遍,value就是修改后传入的新的值
// node.textContent = newValue; // 直接赋值,meihao-18 直接变成 test
});
return this.getValue(vm, args[1]);
});
node.textContent = val;
},
on: function (node, value, vm, type) {
node.addEventListener(type, (e)=>{
vm.$methods[value].call(vm, e); // 注册时间,需要修改this为html的this,因为是在html中注册的
})
}
}
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;
this.proxyData(); // 实现数据拦截
this.$methods = options.methods;
this.$computed = options.computed; // 实现computed计算属性
this.computed2data(); // 将computed中的方法添加到$data中,只有这样将来我们在渲染的时候才能从$data中获取到computed中定义的计算属性
// 根据指定的区域和数据去编译渲染界面
if(this.$el){
// 给外界传入的所有数据都添加get/set方法,这样就可以监听数据的变化了
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) // 在html中的computed中使用this.data,所以需要在这里修改this
}
})
}
}
// 实现数据代理, 将$data上的数据添加到Nue实例上, 这样将来才能通过this.xxx直接获取数据
proxyData(){
for(let key in this.$data){
Object.defineProperty(this, key, {
get: () => { // 外面要使用this.获取,所以这里最好使用箭头函数
return this.$data[key]
}
})
}
}
// 判断是否是一个元素
isElement(node){
return node.nodeType === 1;
}
}
class Compiler {
constructor(vm){
this.vm = vm;
let fragment = this.node2fragment(this.vm.$el); // 1.将网页上的元素放到内存中
console.log(fragment); // 拿到编译存放到内存中的dom
this.buildTemplate(fragment); // 2.利用指定的数据编译内存中的元素
// 3.将编译好的内容重新渲染会网页上
}
node2fragment(app){
let fragment = document.createDocumentFragment(); // 1.创建一个空的文档碎片对象
let node = app.firstChild; // 2.编译循环取到每一个元素
while (node){ // 循环遍历将app对应的dom的每一个节点按顺序剪切到文档碎片对象中
// 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存储了所有元素的文档碎片对象
return fragment; // 等于最初状态的app
}
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; // eg: type text, v-model name
// 针对v-on:click="myFn"指令,name=v-on:click, value=myFn
if(name.startsWith('v-')){
let [directiveName, directiveType] = name.split(':'); // [v-on, click]
let [_, directive] = name.split('-'); // v-model -> [v, model]; v-on -> [v, on]
CompilerUtil[directive](node, value, this.vm, directiveType); // directiveType为事件类型
}
})
}
buildText(node){
let content = node.textContent;
let reg = /\{\{.+?\}\}/gi;
if(reg.test(content)){
// console.log('是{{}}的文本', content);
CompilerUtil['content'](node, content, this.vm);
}
}
}
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])
}
}
}
// obj: 需要操作的对象
// attr: 需要新增get/set方法的属性
// value: 需要新增get/set方法属性的取值
defineRecative(obj, attr, value){
// 如果属性的取值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
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){
// 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
this.observer(newValue);
value = newValue;
dep.notify(); // 修改值的时候触发订阅方法执行
console.log('监听到数据的变化, 需要去更新UI');
}
}
})
}
}
// 想要实现数据变化之后更新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>myVue</title>
<script src="js/nue.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="name">
<div v-on:click="myFn">我是div</div>
<p>{{getName}}</p>
</div>
<script>
// let vue = new Vue({
let vue = new Nue({
el: '#app',
data: {
name: "meihao"
},
methods:{
myFn(){
console.log(this);
console.log(this.name);
}
},
computed:{
getName(){
return this.name + '666';
}
}
});
</script>
</body>
</html>