第一篇,我主要介绍了这种编程思想,那么本文主要是介绍他们是如何实现的,我之前的文章已经对 Object.defineProperty
有了一个详细的解释,那么我们现在就直奔主题吧。
发布者(对象的监听)
我希望我讲的大家通过看一遍和自己调试一遍可以看懂。 我继续拿 yy 关注女主播 这个故事来说事, 女主播现在成了网红,所以她的一举一动都备受宅男的关注。那么得有一个工具来实现对 女主播24小时的跟进,我们才能实时的知道他的动态这个工具用来发布消息:
// 这是一个女主播的对象
let yyNvzhuo = {
name:"女疯子",
sex:"女",
age:16,
msg:"打游戏中"
}
//首先我要监听这个mm
// 对数据进行监听
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
})
}
/**
* data 对象
* key 属性名
* val 属性值
*/
function defineReactive(data, key, val) {
observe(val); // 递归到最底层 val 不是对象时候 就是最底层了
var deps = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(val,'看女主播的信息')
if (Dep.target) { // 判断是否需要添加订阅者
deps.addSub(Dep.target); // 在这里添加一个订阅者
}
return val;
},
set: function (newVal) {
console.log(val,'女主播的信息改变了')
val = newVal;
deps.notify(); //当值改变的时候 触发
}
});
}
observe(yyNvzhuo) //对女主播进行监听,这时候女主播的数据改变 或者 获取 会触发 get 或 set
yyNvzhuo.age // 16 "看女主播的信息"
yyNvzhuo.sex // 女 看女主播的信息
yyNvzhuo.age = 17 女主播的信息改变了
我们这里看下 vue.js里面是怎么写的
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep(); // vue的中间件
var property = Object.getOwnPropertyDescriptor(obj, key); // 获取我们要观察对象属性值
if (property && property.configurable === false) { //看看能不能被修改不能修改就跳出
return
}
// cater for pre-defined getter/setters
var getter = property && property.get; //如果 property存在 把get 属性只给 getter
var setter = property && property.set; // 同上
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val); // childOb是否有被监听
//源码实现和考虑的功能远远比现在我们做的复杂,所以大家暂时仅仅做为参考,如果大家关注量多我会在以后时间里面一步步的讲解
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) { // 中间件是否有寄存的对象
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify(); //发布消息给 订阅函数 即watcher
}
});
}
到这是不是简单易懂呢?接下来我们把这个又控制台打印方式放到 body 中显示
/**
* data 对象数据
* 要绑定的元素
* 要关注的属性名称
*/
function mvvm(data,el,key){
this.data = data;
observe(data)
el.innerHTML = data[key]
}
var ele = document.querySelector('#name');
let htmlnvzhubo = new mvvm(yyNvzhuo,ele,'msg')
//这样我们就把我们要关注 女主播的 msg 打印在了 html
别急,在只是实现了一个初次的绑定 我们还需要在 msg 发生改变的时候 更新 html,说道这里大家应该很清楚了observe
这个函数用来监听函数的变化并且把消息及时的通知出去(发布消息的功能,发布者)
set: function (newVal) {
console.log(val,'女主播的信息改变了')
// 添加一个方法 来实现 el.innerHTML = newVal //显然直接写在这里是简单粗暴的,我们(一)里面讲的设计模式就白讲了
val = newVal;
}
中间件
那么现在我们把思绪拉到 (-)里面 我们 要采用 订阅发布 这种 面向对象的编程思想
需要订阅 、 发布、 和一个中间件 这么一个流程
显然 defineProperty
中的 set 和 get 就是订阅 和 发布 ,为了方便大家对vue源码的理解,这里我的方法和vue的保持一致,同样来实现一个简化版的功能
//中间件
var Dep = function(){
//存消息用
this.subs = []
}
//订阅 get 用来存储订阅者的需求
Dep.prototype.addSub = function(sub){
this.subs.push(sub) // 这里的 fn 就是 dep.targer 它是什么呢是 一个 new 的watcher
}
// 发布 set 执行 我们页面的更新逻辑用来执行 触发订阅者 想要做的事情
Dep.prototype.notify = function(){
this.subs.forEach(fn=>{
fn.updata() // 执行 watcher 里面的 updata
})
}
Dep.targer = null
看到这里大家是不是觉得和我们第一节里面的内容很类似,虽然函数变了,但是思想没有变,毕竟 设计模式 是思想而不是固定的方程式。
中间件源码,这里很类似,我们就不做过多的介绍了
var uid = 0;
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
有了中间件,接下来就是 一个接收消息的人啦:
订阅者(观察者)
这个函数主要作用就是 接收消息并作出事件的反馈那么我们继续实现功能代码
//我们想下这个函数主要用来做的事情
/*
*观察女主播这个对象,当其变化时候做出反应
*1.获取女主播这个对象属性vm
*2.选择一个属性 exp
*3.事件fn
*/
var Watcher = function(vm,exp,fn){
this.vm = vm;
this.exp = exp;
this.fn = fn;
this.value = this.get(); // 将自己添加到订阅器的操作 同时返回 一个当前属性的值
}
Watcher.prototype.update: function () {
this.run(); // 如果监听的属性更新之后触发 run
}
Watcher.prototype.run: function () {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) { // 是否有数据的变更
this.value = value;
this.fn.call(this.vm, value, oldVal);
}
}
Watcher.prototype.get: function () {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数 也可以或是监听哪个值
// 为了把 它
Dep.target = null; // 释放自己
return value;
}
我们同样看下源码,他的代码实现同上面的原理是类似的
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm; // 把我们观察的对象函数 存到 vm里面
if (isRenderWatcher) { // 判断源
vm._watcher = this;
}
vm._watchers.push(this); // 对观察的 对象进行一个存储
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
this.value = this.lazy
? undefined
: this.get();
};
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get () {
// 用来触发 defineProperty 的get
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
/**
* Add a dependency to this directive.
*/
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this); // 中间的存储 this
}
}
};
/**
* Clean up for dependency collection.
*/
Watcher.prototype.cleanupDeps = function cleanupDeps () {
var i = this.deps.length;
while (i--) {
var dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
var tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
};
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
Watcher.prototype.update = function update () {
// 数据更新的时候判断下模式
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
// 和我上面的函数类似的意思,判断数据是否变化了
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
// 下面是vue更细节的考虑,我们不在这里太多的叙述
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
/**
* Depend on all deps collected by this watcher.
*/
Watcher.prototype.depend = function depend () {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};
/**
* Remove self from all dependencies' subscriber list.
*/
Watcher.prototype.teardown = function teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
var i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
};
最后我们把这些链接起来
function mvvm(data, el, exp) {
this.data = data;
observe(data); // 添加了这个对象的监听 但是并没有和 属性绑定在
el.innerHTML = this.data[exp]; // 这个方法 触发了 属性监听的get 方法 并把内容注入到节点里面
/// 实现功能 对指定的内容进行监听,把更新的内容更换出来
new Watcher(this, exp, function (value) {
el.innerHTML = value;
});
//把 this 对象返回 this对象 包含 了 data
return this;
}
var ele = document.querySelector('#name');
var selfVue = new SelfVue(yyNvzhuo, ele, 'msg');
通过这3大块我们实现了双向绑定,和这3个部分的源码分析,双向绑定我们仅仅实现了一个简单 对象 监控,还有其他的内容,且待下回再看 喜欢的点赞吧。