基本概念
数据驱动
数据响应式
- 数据模型仅仅是普通
js
对象 - 当我们修改数据时,视图会进行更新
- 避免繁琐的DOM操作,提高开发效率
双向绑定
- 数据改变,视图改变
- 视图改变,数据也随之改变
v-model
语法糖创建双向数据绑定
数据驱动
- 只需关心数据本身,无需关心数据如何渲染到视图上
vue2.x响应式核心原理
- 当你把一个普通的
JavaScript
对象传入Vue
实例作为 data 选项,Vue
将遍历此对象所有的property
- 使用
Object.defineProperty
把这些property
全部转为getter/setter
。 Object.defineProperty
是ES5
中一个无法shim
的特性,这也就是Vue
不支持IE8
以及更低版本浏览器的原因。
let data = {
msg: 'hello'
};
let vm = {};
Object.defineProperty(vm, 'msg', {
enumerable: true,
configurable: true,
get() {
console.log('get---', data.msg);
return data.msg;
},
set(val) {
console.log('set---', val);
if (val === data.msg) {
return;
}
data.msg = val;
document.getElementById('app').textContent = data.msg;
}
})
// 测试
vm.msg = 'tctj';
console.log(vm.msg)
- 多个属性如何转换
getter/setter
let data = {
msg: 'hello',
count: 10
};
...
function proxyData(data) {
Object.keys(data).forEach((key) => {
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get() {
console.log('get---', key, data[key]);
return data[key];
},
set(val) {
console.log('set---', val);
if (val === data[key]) {
return;
}
data[key] = val;
document.getElementById('app').textContent = data[key];
}
})
});
}
Object. defineProperty的弊端
Object.defineProperty()
无法原生的对数组进行响应式监听- 实现过程中对于深度嵌套的数据,递归消耗大量性能
Object.defineProperty()
这种方式,没办法监听到后续的手动新增删除属性元素,比如数组,直接通过索引去设置和改变值是不会触发视图更新的
vue3.x响应式核心原理
Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。 Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
- 使用
Proxy
来做数据劫持【proxy】 - Proxy 代理整个对象
let data = {
msg: 'hello',
count: 10
};
let vm = new Proxy(data, {
get(target, key) {
console.log('get---', key, target[key]);
return target[key];
},
set(target, key, val) {
console.log('set---', key, val);
if (target[key]=== val) {
return;
}
target[key] = val;
document.getElementById('app').textContent = target[key];
}
})
vm.msg = 'tctj';
console.log(vm.msg);
发布订阅模式
- 订阅者
- 发布者
- 信号中心
class EventEmitter {
constructor() {
this.subs = Object.create(null);
}
// 注册事件
$on(eventType, handler) {
this.subs[eventType] = this.subs[eventType] || [];
this.subs[eventType].push(handler);
}
// 触发事件
$emit(eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach((handler) => {
handler();
});
}
}
}
// 测试
let bus = new EventEmitter();
bus.$on('custom-click', () => {
console.log('on --- custom-click 1 ---');
});
bus.$on('custom-click', () => {
console.log('on --- custom-click 2 ---');
});
bus.$emit('custom-click')
观察者模式
- 观察者(订阅者)——
Watcher
update
:当事件发生时,具体委托的事情
- 目标(发布者)——
Dep
subs
数组:存储所有观察者addSub()
:添加观察者notify()
:当事件发生,调用所有观察者的update()
模拟vue响应式
Vue
功能
- 负责接收初始化的参数
- 负责把
data
中属性注入到vue
实例,转换getter/setter
- 负责调用
observer
监听data
中所有属性的变化 - 负责调用
compiler
解析指令/差值表达式
结构
- $options
- $el
- $data
- _proxyData()
代码
class Vue {
constructor(options) {
// 1. 通过属性保存选项数据
this.$options = options || {};
this.$data = options.data || {};
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
// 2. 把data中的成员转成getter/setter,并注入到vue实例中
this._proxyData(this.$data);
// 3. 调用observer对象,监听数据的变化
// 4. 调用compiler对象,解析指令和差值表达式
}
_proxyData(data) {
// 遍历data中的说有属性
Object.keys(data).forEach(key => {
// 把data所有属性注入到vuew实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (newValue === data[key]) {
return;
}
data[key] = newValue;
}
});
});
}
}
export default Vue;
Observer
功能
- 负责把
data
选项中的属性转换成响应式数据 - data中的某个属性也是对象,递归调用,把该属性的所有属性转成响应式数据
- 数据变化发送通知
结构
- walk(data)
- defineReactive(data, key, value)
Observer-defineReactive
访问属性
- 首先触发
vue
中的get()
,返回data[key]
- 触发
observer
中defineReactive
定义的get()
方法 defineReactive
的get
方法,如果返回data[key]
,会造成递归调用,导致溢出defineReactive
的get
是一个闭包,扩展了val
的作用域
代码
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 1. 判断data是否是对象
if (!data || typeof data !== 'object') {
return;
}
// 2. 遍历data所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
});
}
defineReactive(obj, key, val) {
const me = this;
// 如果val为对象,递归调用,把val的属性也转换成响应式数据
this.walk(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return val;
},
set(newValue) {
if (newValue === val) {
return;
}
val = newValue;
// 解决后期手动添加属性,不能监听到的问题
me.walk(val);
// TODO: 通知
}
})
}
};
export default Observer;
- 在
vue
初始化里调用
import Observer from './observer';
class Vue {
constructor(options) {
...
// 3. 调用observer对象,监听数据的变化
new Observer(this.$data);
...
}
}
Compiler
功能
- 负责编译模板,解析指令/差值表达式
- 负责页面首次渲染
- 当数据发生变化后重新渲染视图
结构
- el
- vm
- compile(el)
- compileElement(node)
- compileText(node)
- isDirective(attrName)
- isTextNode(node)
- isElementNode(node)
具体流程
- 初始化传入vm,保存元素节点
- 初始化时执行
compile()
,进行首次渲染compile
方法中,遍历所有子节点- 判断如果是文本节点,则执行
compileText()
compileText
中,利用正则,匹配出key
,并进行value
替换
- 判断如果是元素节点,则执行
compileElement()
compileElement
中,遍历元素的属性,如果是指令属性,则进行update
编译
- 判断如果是文本节点,则执行
代码
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
this.compile(this.el);
}
// 编译模板,处理文本节点元素节点
compile(el) {
let childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isTextNode(node)) {
// 处理文本节点
this.compileText(node);
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node);
}
// 判断如果有子节点,递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
// 编译元素节点,处理指令
compileElement(node) {
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name;
if (this.isDirective(attrName)) {
// v-text -> text
attrName = attrName.substr(2);
let key = attr.value;
this.update(node, key, attrName);
}
});
}
// 更新
update(node, key, attrName) {
const updateFn = this[attrName + 'Updater'];
updateFn && updateFn.call(this, node, this.vm[key]);
}
// 处理v-text指令
textUpdater(node, value) {
node.textContent = value;
}
// 处理v-model指令
modelUpdater(node, value) {
node.value = value;
}
// 编译文本节点,处理差值表达式
compileText(node) {
// console.dir(node)
// 正则匹配 {{xx}}
let reg = /\{\{(.+?)\}\}/ig;
let value = node.textContent;
if (reg.test(value)) {
let key = RegExp.$1.trim();
node.textContent = value.replace(reg, this.vm[key]);
}
}
// 判断元素属性是否是指令
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 判断是否是文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 判断是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
};
export default Compiler;
Dep
功能
- 收集依赖,添加观察者
- 通知所有观察者
结构
- subs
- addSub(sub)
- notify()
代码
class Dep {
constructor() {
// 存储所有观察者
this.subs = [];
}
// 添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 发送通知
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
};
export default Dep;
- 为响应式数据创建发布者
// observer.js
import Dep from './dep';
class Observer {
...
defineReactive(obj, key, val) {
...
const dep = new Dep();
...
Object.defineProperty(obj, key, {
...
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target);
...
}
set() {
...
// 数据更新,发送通知
dep.notify();
}
}
}
}
Watcher
功能
- 当数据变化触发依赖,
dep
通知所有的Watcher
实例更新视图 - 自身实例变化时往dep对象中添加自己
结构
- vm
- key
- cb
- oldValue
- update()
代码
import Dep from "./dep";
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
// 回调函数-负责更新视图
this.cb = cb;
// data中属性名称
this.key = key;
// 1. 把watcher对象记录到Dep类的静态属性target中
Dep.target = this;
// 2. 触发get,get中会调用addSub收集依赖
this.oldValue = vm[key];
// 3. 收集完后,设置为空
Dep.target = null;
}
// 当数据发生变化时更新视图
update() {
let newValue = this.vm[this.key];
if (this.oldValue === newValue) {
return;
}
this.cb(newValue);
}
};
export default Watcher;
- 在数据更新的地方,创建watcher,
// compiler.js
import Watcher from './watcher';
class Compiler {
...
textUpdater(node, key, value) {
node.textContent = value;
new Watcher(this.vm, key, newValue => {
node.textContent = newValue
});
}
modelUpdater(node, key, value) {
node.value = value;
new Watcher(this.vm, key, newValue => {
node.value = newValue
});
}
compileText(node) {
...
if (reg.test(value)) {
let key = RegExp.$1.trim();
node.textContent = value.replace(reg, this.vm[key]);
// 创建watcher对象,当数据改变时更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
}
}
双向绑定
- 处理
v-model
的双向绑定
// compiler.js
...
modelUpdater() {
...
// 双向绑定
node.addEventListener('input', ev => {
this.vm[key] = node.value;
});
}