数据双向绑定简易原理
<input type="text" id="username">
<span id="uName"></span>
let obj = {}
Object.defineProperty(obj, 'username', {
// 使用 defineProperty 中的 get 来双向绑定数据
set (v) {
document.getElementById('uName').innerText = v;
document.getElementById('username').value = v;
},
get () {
console.log('get')
}
})
document.getElementById('username').addEventListener('keyup', function (e) {
obj.username = e.target.value
})
defineProperty 实现原理
const vm = {
name: 'bob',
age: 11,
enjoy: ['1', 'a', 3],
hobby: { a: 'eat' }
}
const orginalProto = Array.prototype
const arrayProto = Object.create(orginalProto) // 先克隆份 Array 原型
const methodToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodToPatch.forEach(method => {
arrayProto[method] = function () {
console.log('method changed: ', method, arguments)
orginalProto[method].apply(this, arguments)
}
})
const observe = (data) => {
if (!data || typeof data !== 'object') return
if (Array.isArray(data)) {
// 如果是数组,重写原型链 data.__proto__ = arrayProto
Object.setPrototypeOf(data, arrayProto);
for (let i = 0; i < data.length; i++) {
observe(data[i])
}
} else {
Object.keys(data).forEach(key => {
dr(data, key, data[key])
})
}
}
const dr = (data, key, val) => {
observe(val) // 递归制造响应式数据
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
set(nv) {
console.log('val changed: ', nv)
val = nv
},
get() {
return val
}
})
}
observe(vm)
vm.name = 'lucy'
vm.age = 11
vm.hobby.a = 'play'
vm.enjoy.push(123)
vm.sex = 'female' // 监听不到
// 问题:直接 watch 对象下某个属性监听不到
@Watch('searchList', { deep: true, immediate: true })
onSearchListWatch(newVal, oldVal) {
const getSubBu = (list) => list?.find((item) => item.index === 1)?.subBuCode;
if (getSubBu(newVal) !== getSubBu(oldVal)) {
this.initLineList();
}
}
// 解决
@Watch('oneSearchList', { deep: true, immediate: true })
onSearchListWatch(newVal, oldVal) {
if (newVal !== oldVal) {
this.initLineList();
}
}
get oneSearchList() {
return this.searchList?.find((item) => item.index === 1)?.subBuCode;
}
缺陷
Object.defineProperty 无法监听新增加的属性
Object.defineProperty 无法一次性监听对象所有属性,如对象属性的子属性
Object.defineProperty 无法响应数组操作
Proxy 拦截方式更多, Object.defineProperty 只有 get 和 set
const vm = {
name: 'bob',
age: 11,
enjoy: ['1', 'a', 3],
hobby: { a: 'eat' }
}
const observe = (data) => {
if (!data || typeof data !== 'object') return
Object.keys(data).forEach(key => {
if (typeof data[key] === 'object') {
data[key] = proxyData(data[key])
observe(data[key])
}
})
return proxyData(data);
}
// Reflect
// Reflect 是一个内建对象,可简化 Proxy 的创建。
// 前面所讲过的内部方法,例如 [[Get]] 和 [[Set]] 等,都只是规范性的,不能直接调用。
// Reflect 对象使调用这些内部方法成为了可能。它的方法是内部方法的最小包装。
const proxyData = (data) => new Proxy(data, {
get(target, propKey, receiver) {
return Reflect.get(target, propKey, receiver)
},
set(target, propKey, value, receiver) {
console.log('val changed: ', value)
// target[propKey] = value
Reflect.set(target, propKey, value, receiver)
return true
}
})
const proxyVm = observe(vm);
proxyVm.name = 'lucy'
proxyVm.age = 11
proxyVm.hobby.a = 'play'
proxyVm.enjoy.push(123)
proxyVm.sex = 'female'
3.0 改进
为什么使用 Proxy 可以解决上面的问题呢?主要是因为Proxy是拦截对象,对对象进行一个"拦截",外界对该对象的访问,都必须先通过这层拦截。无论访问对象的什么属性,之前定义的还是新增的,它都会走到拦截中
vue 实现简易原理,其内部主要由 Observer、Compile、Watcher 组成,中间包括 Dep(变化通知)以及 Updater(视图更新)等模块,最终再由 MVVM(new Vue)去汇合
区分 Proxy 和 Decorator 的使用场景,可以概括为:Proxy 的核心作用是控制外界对被代理者内部的访问,Decorator 的核心作用是增强被装饰者的功能
Proxy 可以理解成在目标对象前架设一个“拦截”层,外界对该对象的访问都必须先通过这层拦截,因此提供了一种机制可以对外界的访问进行过滤和改写
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)
Proxy 用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改
Proxy 性能问题
Proxy 的性能比 Promise 还差
Proxy 作为新标准,从长远来看,JS 引擎会继续优化 Proxy
Proxy 兼容性差(Vue 3.0 中放弃了对于IE的支持)
Observer
- 利用 Obeject.defineProperty() 来监听属性变动
class Observer {
constructor(data) {
this.data = data;
this.walk(data);
}
walk(data) {
var me = this;
Object.keys(data).forEach(function (key) {
me.convert(key, data[key]);
});
}
convert(key, val) {
this.defineReactive(this.data, key, val);
}
defineReactive(data, key, val) {
var dep = new Dep();
observe(val); // 监听子属性
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get() {
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是 object 的话,进行监听
if (typeof newVal === 'object') observe(newVal);
// 通知订阅者
dep.notify();
}
});
}
}
- 那么将需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};
- 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化,其中通过 Dep 来作为调度者,对 Watcher 通知变化以及添加订阅者
var uid = 0;
class Dep {
constructor() {
this.id = uid++;
this.subs = [];
}
addSub(sub) { // 添加订阅
this.subs.push(sub);
}
depend() {
// 此时 Dep.target 指向 Watcher 的 this
Dep.target.addDep(this);
}
removeSub(sub) { // 移除订阅
var index = this.subs.indexOf(sub);
if (index != -1) {
this.subs.splice(index, 1);
}
}
notify() { // 通知订阅者
this.subs.forEach(function (sub) {
// sub 为 getter 时添加到 subs 的 Watcher 的前一个状态
sub.update();
});
}
}
Dep.target = null;
Watcher
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
- 1、在自身实例化时往属性订阅器 (dep) 里面添加自己
- 2、自身必须有一个 update() 方法
- 3、待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退
class Watcher {
constructor(vm, expOrFn, cb) {
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
this.depIds = {};
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
// 此处为了触发属性的 getter,从而在 dep 添加自己,结合 Observer 更易理解
this.value = this.get();
}
update() {
this.run();
}
run() {
var value = this.get(); // 收到最新值
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
addDep(dep) {
// 1. 每次调用run()的时候会触发相应属性的getter
// getter里面会触发dep.depend(),继而触发这里的addDep
// 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
// 则不需要将当前watcher添加到该属性的dep里
// 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
// 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
// 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
// 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
// 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
// 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
// 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
// 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
// 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
// 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
// dep 指向 Observer 的 this
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this); // 添加当前状态的 this 对象(value、expOrFn 等等)到 Observer
this.depIds[dep.id] = dep;
}
}
get() {
Dep.target = this; // 将当前订阅者指向自己
var value = this.getter.call(this.vm, this.vm); // 为 getter 传入自身的 obj,从而获取值
Dep.target = null; // 添加完毕,重置
return value;
}
parseGetter(exp) {
if (/[^\w.$]/.test(exp)) return;
var exps = exp.split('.');
return function (obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
}
compile
- compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,
- 并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
const compileUtil = {
text(node, vm, exp) { // v-text
this.bind(node, vm, exp, 'text');
},
html(node, vm, exp) { // v-html
this.bind(node, vm, exp, 'html');
},
bind(node, vm, exp, dir) { // 绑定更新节点
var updaterFn = updater[dir + 'Updater'];
// 第一次初始化视图
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 实例化订阅者,此操作会在对应的属性消息订阅器(有绑定的对象值)中添加了该订阅者 watcher
new Watcher(vm, exp, function (value, oldValue) {
// 一旦属性值有变化,会收到通知执行此更新函数,更新视图
updaterFn && updaterFn(node, value, oldValue);
});
},
// ...省略
}
更新 updater
const updater = { // 更新节点
textUpdater(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
htmlUpdater(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
},
classUpdater(node, value, oldValue) {
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');
var space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},
modelUpdater(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
};
compile
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
node2Fragment(el) { // 虚拟 dom
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
// fragment.appendChild() 具有移动性,此操作是 move dom
// 将 el.children[0] 被抽出,在下次操作从 el.children[1] 开始,以达到循环的目的
fragment.appendChild(child);
}
return fragment;
}
init() {
this.compileElement(this.$fragment);
}
// 递归遍历所有节点及其子节点,进行扫描解析编译,调用对应指令渲染,并调用对应指令更新函数进行绑定
compileElement(el) {
var childNodes = el.childNodes,
me = this;
[].slice.call(childNodes).forEach(function (node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/;
// 按元素节点方式编译
if (me.isElementNode(node)) { // 是否元素节点
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) { // 是否文本节点并符合 {{xxx}} 形式
// 指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
// 以此类推,RegExp.$2,RegExp.$3,..RegExp.$99 总共可以有99个匹配
me.compileText(node, RegExp.$1.trim());
}
// 遍历编译子节点
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
}
// 修改元素节点对应属性的赋值操作
compile(node) {
var nodeAttrs = node.attributes,
me = this;
[].slice.call(nodeAttrs).forEach(function (attr) {
var attrName = attr.name;
// 规定:指令以 v-xxx 命名(v-text、v-html 等)
if (me.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2);
if (me.isEventDirective(dir)) {
// 事件指令,如 v-on:click
compileUtil.eventHandler(node, me.$vm, exp, dir);
} else {
// 普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
node.removeAttribute(attrName);
}
});
}
compileText(node, exp) {
compileUtil.text(node, this.$vm, exp);
}
isDirective(attr) {
return attr.indexOf('v-') == 0;
}
isEventDirective(dir) {
return dir.indexOf('on') === 0;
}
isElementNode(node) {
return node.nodeType == 1;
}
isTextNode(node) {
return node.nodeType == 3;
}
}
MVVM
- MVVM作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,
- 通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;
- 视图交互变化 (input) -> 数据 model 变更的双向绑定效果
class MVVM {
constructor(options) {
this.$options = options || {};
var data = this._data = this.$options.data;
var me = this;
// 属性代理,实现 vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function (key) {
me._proxyData(key);
});
this._initComputed();
// 对所有对象启动监听
observe(data, this); // new Observer
// 启动编译
this.$compile = new Compile(options.el || document.body, this)
}
// 监听需要用到的值
$watch(key, cb, options) {
new Watcher(this, key, cb);
}
// 代理 this 中的 data
_proxyData(key, setter, getter) {
var me = this;
setter = setter ||
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}
// 代理 this 中的 computed
_initComputed() {
var me = this;
var computed = this.$options.computed;
if (typeof computed === 'object') {
Object.keys(computed).forEach(function (key) {
Object.defineProperty(me, key, {
get: typeof computed[key] === 'function'
? computed[key]
: computed[key].get,
set: function () { }
});
});
}
}
}