一、Vue响应式原理图:
二、Vue响应式原理
Vue是采用数据劫持配合发布者-订阅者模式的方式实现响应式的。通过Object.defineProperty
来劫持各个属性的setter和getter,在数据变动时,发布消息给依赖收集器Dep, 去通知观察者Watcher,由Watcher执行更新视图的回调函数。
MVVM的实现整合了Observer、CompiIe和watcher三者。他通过Observer 来劫持监听model数据,通过Compile来解析编译模板指令并对数据绑定对应的观察者,最终利用Watcher搭起Observer、CompiIe之间的通信桥梁,以达到数据变化 → 视图更新;视图交互变化 → model数据变更的双向绑定效果。
三、Vue相应式原理图解
第 1 步:Observer内通过Object.defineProperty
对数据进行劫持,并绑定getter和setter。
getter用于返回数据并通知Dep收集订阅器。
setter用于设置新值并通知Dep数据变化
第 2 步:Compile对指令(v-text、v- html、{{ }} 等)进行解析,并准备初始化页面。
第 3 步:初始化页面前需要执行3,实例化Watcher,为即将渲染到页面的数据绑定自身对应的watcher(watcher中有更新视图的回调函数)。
第 4 步:拿数据时会触发getter,getter将数据返回用于渲染页面,并执行4-1,告诉Dep收集之前为该数据绑定的watcher,然后Dep就执行4-2去收集watcher。
第 5 步:当数据变化时会触发setter,setter为数据设置新值,并执行5,告诉Dep数据变化了,Dep又通知Watcher数据变化了,又watcher执行回调去更新页面。
数据变化响应视图: 当数据变化时会触发setter,setter为数据设置新值,并执行5,告诉Dep数据变化了,Dep又通知Watcher数据变化了,又watcher执行回调去更新页面。
据变化了, Wat ( her 会执行回调更新页面。
视图变化响应数据: 当视图变化时,触发事件( 如input 标签的input 事件)对数据进行改变,数据改变会触发setter,然后又执行第5步 。
四、Vue响应式代码模拟实现(不到300行)
1.Vue入口(25行)
实例化Observer、Watcher等功能模块,并对data、methods、computed进行代理,即实现【this.数据】而不是【this.data.数据】、【this.方法】而不是【this.methods.方法】等。
// Vue入口
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实例化数据劫持监听--Object.defineProperty
new Observer(this.$data);
// 2.实例化指令解析器
new Compile(this.$el, this);
// 对data进行代理实现 this.person.name。methods、computed的代理实现雷同。
this.proxyData(this.$data);
}
}
// 对data中定义的数据进行代理,相当于挂载到了vm实力上,可以直接【this.数据】来使用,而不用【this.data.数据】
proxyData(data) {
for (const key in data) {
// 对data第一层做劫持绑定get和set。this指向vm
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
})
}
}
}
2.Observer数据劫持和监听的实现(44行)
Observer主要是对数据进行劫持监听,通过Object.defineProperty
对数据设置set和get,get返回数据并通知收集watcher,set设置数据并通知Dep数据变化。这里还实现了watcher的收集器Dep,Dep主要又两个功能,收集watcher和通知watcher数据变化。
// 通知watcher数据变化需更新视图 和 收集每个数据对应watcher的作用
class Dep {
constructor() {
this.subs = []
}
// 收集观察者
addSub(watcher) {
this.subs.push(watcher);
}
// 通知观察者更新视图
notify() {
console.log(this)
this.subs.forEach(w => w.update());
}
}
// 劫持监听数据
class Observer {
constructor(data) {
this.observer(data);
}
// 观察对象数据
observer(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach((key) => {
// 劫持监听数据
this.defineReactive(data, key, data[key]);
})
}
}
// 劫持监听数据
defineReactive(data, key, value) {
// 递归遍历 每一层
this.observer(value);
// 实例化Dep存储watcher和通知watcher更新视图。
const dep = new Dep();
// 劫持每个属性,Object.defineProperty(目标对象,劫持的键值,键值对应的属性的特性)
Object.defineProperty(data, key, {
enumerable: true, // 可以被枚举(使用for...in或Object.keys())
configurable: false, // 是否可再次被设置属性特性
get() { // 获取属性值
// 得到数据的同时向Dep中添加观察者订阅器(每个数据都有自己的watcher)
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newValue) => { // 修改属性,使用箭头函数获取定义上下文的this,普通函数this指向了传进来的data
// 防止新修改的属性不被劫持监听
this.observer(newValue);
if (newValue !== value) {
value = newValue;
// 数据修改通知对应watcher
dep.notify();
}
}
})
}
}
3.Compile指令解析初始化页面的实现(137行)
Compile主要实现了对指令的解析、数据绑定watcher、初始化页面的功能。其中包含:指令解析的工具类compileUtil、初始化页面工具类updater、Compile自身。已实现的解析指令有v-text、v-html、v-model、v-bind、v-on、{{}}、:、@
// 具体指令解析操作
const compileUtil = {
// node当前节点,expr指令值,vm实例
text: function (node, expr, vm) { // v-text 解析
// 插值表达式单独处理
if (expr.indexOf('{{') !== -1 && expr.indexOf('}}') !== -1) {
// 获取插值表达式对应的值{{person.name}}---{{person.age}}
const value = expr.replace(/\{\{(.+?)\}\}/g, (...arg) => {
new Watcher(vm, arg[1], () => {
// 插值表达式可能是复杂的,比如同时有多个,不能只处理一个就替换,所以需要单独处理
return updater.textUpdater(node, this.getInterpolation(expr, vm));
})
return (this.getVal(arg[1], vm));
})
// 渲染页面
return updater.textUpdater(node, value);
}
// 绑定watcher,订阅数据变化
this.bindWatch(node, expr, vm, 'text');
},
getInterpolation(expr, vm) {
// 插值表达式添加watcher做单独处理
return expr.replace(/\{\{(.+?)\}\}/g, (...arg) => {
return (this.getVal(arg[1], vm));
})
},
html: function (node, expr, vm) { // v-html 解析
this.bindWatch(node, expr, vm, 'html');
},
model: function (node, expr, vm) { // v-model 解析
this.bindWatch(node, expr, vm, 'model');
// 视图 → 数据 → 视图
node.addEventListener('input', (e) => {
this.setVal(expr, vm, e.target.value)
}, false);
},
bind: function (node, expr, vm, attrName) { // v-bind 和 : 解析
// expr:指令值, attrName:绑定的属性名
this.bindWatch(node, expr, vm, 'bind', attrName);
},
on: function (node, expr, vm, eventName) { // v-on 和 @ 解析
// expr:方法名, eventName:事件
// 获取事件方法
const fun = vm.$options.methods && vm.$options.methods[expr];
// 小细节:vue的methods中的方法this默认指向vm实例,而这里fun将被node调用,所以this是指向node节点的。
node.addEventListener(eventName, fun.bind(vm), false)
},
bindWatch: function (node, expr, vm, directive, attrName) {
// node:节点, expr:指令值(data), vm:实例, directive:指令种类, attrName:绑定属性值(v-bind和: 才有)
// 指令触发的时候需要为其添加watcher的回调订阅
const updaterFn = updater[directive + 'Updater'],
value = this.getVal(expr, vm); // 获取对应的渲染函数,获取指令绑定数据
// 触发渲染
updaterFn && updaterFn(node, value, attrName);
new Watcher(vm, expr, (newVal) => {
updaterFn && updaterFn(node, newVal, attrName); // 数据更新,watcher回调,更新视图
})
},
getVal: function (expr, vm) {
// expr形式可能是msg也可能是person.name,所以需要遍历获取expr对应的vm实例data中的值
return expr.split('.').reduce((data, current) => {
return data[current];
}, vm.$data)
},
setVal: function (expr, vm, inputVal) {
let base = vm.$data; // 所有数据取值都是从这里开始的
expr = expr.split('.'); // 为了遍历方便将其分割成数组
expr.forEach((value, index) => {
// 形如:person.son.name这种嵌套对象,只有当到达最后一层时,才将新值赋值。
if (index < expr.length - 1) {
base = base[value]
} else {
base[value] = inputVal; // 更新新值
}
})
}
}
// 指令解析后更新页面
const updater = {
// node当前节点,value当前值
textUpdater: function (node, value) { // v-text 渲染
// 当value不存在时 应该显示 '' 而不是undefined
node.textContent = typeof value == 'undefined' ? '' : value;
},
htmlUpdater: function (node, value) { // v-html 渲染
node.innerHTML = typeof value == 'undefined' ? '' : value;
},
modelUpdater: function (node, value) { // v-model 渲染
node.value = typeof value == 'undefined' ? '' : value;
},
bindUpdater: function (node, value, attrName) { // v-bind 渲染
node.setAttribute(attrName, value);
}
}
// 指令解析器
class Compile {
// 接收根节点对象el,和vm实例
constructor(el, vm) {
// 得到元素节点
this.el = this.isElementNode(el) ? el : document.querySelector(el);
// vm实例
this.vm = vm;
// 1.为了避免频繁操作dom,造成回流重绘等浪费性能问题,这里使用文档碎片存储dom
const fragment = this.nodeFragment(this.el);
// 2.对fragment进行模板编译,处理指令、插值表达式、事件等,有fragment就减少了dom操作
this.compileInit(fragment);
// 3.将文档碎片插入到dom中
this.el.appendChild(fragment);
}
// 对fragment进行模板编译
compileInit(fragment) {
// 获取文档碎片下的所有子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
// 是元素节点
this.compileElement(child);
} else if (this.isTextNode(child)) {
// 是文本节点
this.compileText(child);
}
if (child.childNodes && child.childNodes.length) {
// 元素嵌套的递归
this.compileInit(child)
}
})
}
// 编译元素节点
compileElement(node) {
const nodeAttr = node.attributes;
[...nodeAttr].forEach(attr => {
// 解构属性值和名
const {
name,
value
} = attr;
if (this.isDirective(name)) {
// 是否是指令
// directive 值为 text, html, model, on:click, bind:XXX 仅做这几种
const [, directive] = name.split('-');
const [dirName, eventName] = directive.split(':'); // ['text','']['html','']['model','']['on','click']……
// 对应指令的解析事件,传递(当前节点,指令值,vm实例,事件)
compileUtil[dirName](node, value, this.vm, eventName)
// 删除标签上的 v-html v-text 之类的属性
node.removeAttribute('v-' + directive);
} else if (this.isAtSymbol(name)) { // 是否是@开头的指令
const [, eventName] = name.split('@');
// 执行v-on 解析
compileUtil['on'](node, value, this.vm, eventName)
// 删除标签上的 @开头的 属性
node.removeAttribute('@' + eventName);
} else if (this.isColonSymbol(name)) {
const [, attrName] = name.split(':');
// 执行v-bind 解析
compileUtil['bind'](node, value, this.vm, attrName)
// 删除标签上的 :开头的 属性
node.removeAttribute(':' + attrName);
}
})
}
// 编译文本节点
compileText(node) {
// 匹配所有 带有“{{}}”进行解析
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
// 调用解析
compileUtil['text'](node, content, this.vm)
}
}
// 判断是否是以“v-”开头的属性
isDirective = (directive) => directive.startsWith('v-');
// 判断是否以“@”开头
isAtSymbol = (directive) => directive.startsWith('@');
// 判断是否以“:”开
isColonSymbol = (directive) => directive.startsWith(':');
// 将dom文档碎片化
nodeFragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
// 每次使用appendChild()方法后el节点的firstChild会被移除
// 这不是死循环!!!
f.appendChild(firstChild);
}
return f;
}
// 判断是不是元素节点
isElementNode = (node) => node.nodeType === 1;
// 判断是不是文本节点
isTextNode = (node) => node.nodeType === 3;
}
4.Watcher订阅数据更新视图的实现(20行)
Watcher主要功能为获取数据旧值以及更新视图的功能函数。
// 观察者watcher,作用更新视图
class Watcher {
constructor(vm, expr, callback) {
this.vm = vm; // 实例
this.expr = expr; // 被观察的数据
this.callback = callback; // 回调函数更新视图
this.oldVal = this.getOldVal() // 旧值
}
// 获取旧值,使用指令解析中的compileUtil中的getVal方法
getOldVal() {
// 观察者被创建时会收集旧值,此时将该变量的watcher实例挂到Dep上。
Dep.target = this; // this指向Watcher的实例
const oldVal = compileUtil.getVal(this.expr, this.vm);
delete Dep.target; // 旧值获取完毕清除Dep上的watcher
return oldVal;
}
// 更新视图 -- 新值和就值是否有变化,有执行回调更新视图
update() {
// 数据变化后,Dep通知watcher 更新视图,所以此处获取到的是新值。
const newVal = compileUtil.getVal(this.expr, this.vm);
if (newVal !== this.oldVal) {
// 新值和就值不同时,更新视图。
this.callback(newVal);
}
}
}
五、测试使用
<div id='app'>
<h1>插值表达式:{{person.name}}---{{person.age}}</h1>
<div>插值表达式:{{msg}}</div>
<p v-text="msg"></p>
<ul>
<li>文本1</li>
<li>插值表达式:{{msg}}</li>
<li>文本2</li>
</ul>
<div v-html="htmlMsg"></div>
<input type="text" v-model="person.age">
<input type="text" v-model="msg">
插值表达式:{{person.name}}
<button v-on:click="handle">v-on绑定事件→点我</button>
<button @click="handle">@绑定事件→点我</button>
<div v-bind:title="msg">v-bind绑定title</div>
<div :title="msg">:绑定title</div>
{{msg}}
</div>
<script src="./Watcher.js"></script>
<script src="./Observer.js"></script>
<script src="./Compile.js"></script>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
person: {
name: '张三',
age: 19
},
htmlMsg: '<strong><i>我是v-html</i></strong>',
msg: 'vue双向绑定',
},
methods: {
handle: function () {
console.log(this)
// this.person.name='李四'
alert('点击事件触发!我将修改数据');
}
}
})
</script>
如果不明白可以看这里:vue的响应式、双向绑定
到这里就模拟实现了vue的响应式、双向绑定,只是模拟,不要较真。。。
如果对你有帮助可以点赞👍+收藏哦~~~我们一起学前端。
以上内容均为原创,转载请注明来源!!!!