实现vue2的响应式原理
参考视频:www.bilibili.com/video/BV193…
第一步:先定义好html的基本结构
div id="app">
<span>创始人:{{name}}</span>
<input type="text" v-model="name">
<span>更多:{{more.like}}</span>
<input type="text" v-model="more.like">
</div>
第二步:定义Vue中实现响应的的data数据
const vm = new Vue({
el: "#app",
data: {
name: '影子',
more: {
like: '星心'
}
}
})
这里我们监听两个属性name和more,属性more的值又是一个对象。
第三步:直接看代码(配有详细注释,注意每个小点(1,2,...)的逻辑跳跃)
//一、首先我们创建一个Vue的类
class Vue {
constructor(obj_instance) {
// 1.将传过来的对象(obj_instance其实就是new Vue实例)身上的data属性传给当前实例里的$data属性
this.$data = obj_instance.data;
Observer(this.$data); //对data里面的每个属性进行数据劫持
Compile(obj_instance.el, this); //HTML解析模板
}
}
二、数据劫持
// 2.数据劫持-监听实例里面的数据
function Observer(data_instance) {
// 4.为递归找出口,当当前的属性为空或者没有检测到当前属性是对象时需要终止递归
if (!data_instance || typeof data_instance !== 'object') return;
const dependency = new Dependency();
// Object.keys以数组的形式返回对象里面的每个key(属性),可以通过其遍历每个属性并对每个属性进行数据劫持
Object.keys(data_instance).forEach(key => {
let value = data_instance[key];//在进入Object.defineProperties之前先存下属性的值,否则当进入Object.defineProperties之后属性的值就已经发生改变了
// 3.递归,对当前属性的值进行数据劫持,否则只能劫持到最表面的一层属性
Observer(value)
Object.defineProperty(data_instance, key, {
enumerable: true,//表示属性可枚举
configurable: true,//表示属性可修改
get() {
// console.log(`访问了属性${key}->值:${value}`);
// 订阅者加入依赖实例的数组(Dependency.temp不为空说明当前有新的订阅者,将新的订阅者加入到订阅者数组中)
Dependency.temp && dependency.addSub(Dependency.temp)
return value;
},
set(newValue) {
// console.log(`属性${key}的值${value}修改为->${newValue}`)
value = newValue
Observer(newValue);//递归,对新设置的值进行劫持监听,以防设置新的值也是一个对象时没有及时劫持到,如果传入的不是对象会直接return,如果传入的是对象会进行数据劫持的操作
// 在修改数据的时候需要通知订阅者来更新
dependency.notify();//当设置新的值的时候需要调用依赖的通知方法,通知所有订阅者更新数据
},
})
})
}
// HTML模板解析-替换DOM
function Compile(element, vm) {//element表示挂载到哪个元素,vm表示Vue实例
vm.$el = document.querySelector(element);
const fragment = document.createDocumentFragment();
let child;
while (child = vm.$el.firstChild) {
fragment.append(child);//将vm.$el身上的所有节点属性文本等都移到碎片文档中
}
fragment_compile(fragment);
// 替换文档碎片内容
function fragment_compile(node) {
const pattern = /\{\{\s*(\S+)\s*\}\}/;//设置正则表达式来匹配html结构中的模板字符串部分
if (node.nodeType === 3) { //文本的nodeType值为3对应的是span里面的文本
const xxx = node.nodeValue;//提前保存这个值
const result_regex = pattern.exec(node.nodeValue);//返回匹配成功结果的数组
if (result_regex) {
const arr = result_regex[1].split('.');//result_regex[1]是匹配出来的属性名,split('.')针对当属性名是多个‘.’连接的属性名时将其切割
const value = arr.reduce((total, current) => total[current], vm.$data)
node.nodeValue = xxx.replace(pattern, value)
// 创建订阅者(result_regex[1]对应当前的属性 name 和 more.like )
new Watch(vm, result_regex[1], (newValue) => {
node.nodeValue = xxx.replace(pattern, newValue)
});
}
return
}
if (node.nodeType === 1 && node.nodeName === 'INPUT') { //v-model绑定,获取节点值为1的节点(即input节点)
const attr = Array.from(node.attributes);
Array.from(node.attributes).forEach(i => {
if (i.nodeName === 'v-model') {
const value = i.nodeValue.split('.').reduce((total, current) => total[current], vm.$data); //获取将当前属性的值
node.value = value;
// 创建订阅者(i.nodeValue对应当前的属性 name 和 more.like )
new Watch(vm, i.nodeValue, newValue => {
node.value = newValue;
});
node.addEventListener('input', e => {
// ['more','like']
const arr1 = i.nodeValue.split('.');
//['more']
const arr2 = arr1.slice(0, arr1.length - 1);
// vm.$data.more
const final = arr2.reduce((total, current) => total[current], vm.$data);
final[arr1[arr1.length - 1]] = e.target.value;
})
}
});
}
node.childNodes.forEach(child => fragment_compile(child))
}
vm.$el.appendChild(fragment)
}
// 依赖-收集和通知订阅者
class Dependency {
constructor() {
this.subscribers = [];//存放订阅者
}
// 添加订阅者
addSub(sub) {
this.subscribers.push(sub);
}
// 通知订阅者
notify() {
this.subscribers.forEach(sub => sub.update());
}
}
// 订阅者
class Watch {
constructor(vm, key, callback) {
this.vm = vm;//vm表示Vue实例
this.key = key;//key表示Vue实例对应的属性
this.callback = callback;//callback记录如何更新文本内容、
Dependency.temp = this;//临时属性-触发getter,说明此时有订阅者
key.split('.').reduce((total, current) => total[current], vm.$data)
Dependency.temp = null;//触发getter之后将其情况,防止订阅者多次加入到依赖实例数组里面
}
// 发布者通知订阅者可以更新了
update() {
const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data)
this.callback(value);
}
}