( 第三篇 )仿写'Vue生态'系列___" '枚举' 与 '双向绑定' "
本次任务
- 对'遍历'这个名词进行死磕.
- 对defineProperty进行分析.
- 实现cc_vue的数据双向绑定.
- 为下一篇 Proxy 代替 defineProperty 做预热.
一. 'forEach' vs 'map'
很多文章都写过他们两个的区别
- forEach没有返回值, map会返回一个数组
- map利于压缩, 因为毕竟只有三个字母.
但是这些区别只是表面上的, 我来展示一个有趣的👇
<div id="boss">
<div>1</div>
<div>2</div>
<div>3</div>
</div>
<script>
let oD = document.getElementById('boss');
// 正常执行
oD.childNodes.forEach(element => {console.log(element); });
// 报错
oD.childNodes.map(element => { console.log(element); });
</script>
oDs.childNodes 并不是个数组, 他仍然是伪数组, 但是他可以使用forEach, 这个挺唬人的, 第一反应是这两个遍历方法在实现的方式上是不是有什么不同, 但转念一想感觉自己想歪了, 答案其实是 oDs.childNodes这个伪数组形成的时候, 往身上挂了个forEach...
通过上面的问题我有了些思考
- map既然返回新数组, 那就说明他空间复杂度会大一些.
- 某些系统提供的伪数组, 本身会挂载forEach但不会挂载map.
综上所述, 还是用forEach保险!
但是就想用map怎么办那?
1: slice的原理就是一个一个的循环放入一个新数组;
let slice = Array.prototype.slice;
slice.call(oD.childNodes).map(()=>{})
2: 扩展运算符原理不太一样, 但他一样可以把所有元素都拿出来, 接下来我们就对他进行死磕.
[...oD.childNodes].map(()=>{})
二. 扩展运算符
这个神奇的语法其实有很多门道的, 我们来一起死磕一下吧.
下面代码会正确执行, 对象放入对象肯定没问题
let obj = {a:1,b:2},
result = {...obj};
console.log(result)
下面代码会报错, 因为缺少iterable.
let obj = {a:1,b:2,length:2},
result = [...obj];
console.log(result)
原因是'扩展运算符'不知道该怎么扩展他, 我们要告诉如何扩展才可以正确的执行.
Symbol.iterator是Symbol身上的属性, 而iterable的key就是它.
let obj = { '0': 'a', '1': 'b', length: 2 };
obj[Symbol.iterator] = function() {
let n = -1,
_this = this,
len = this.length;
// 必须有返回值
// 并且返回值必须是对象
return {
// 必须有next
next: function() {
n++;
if (n < len) {
return {
value: _this[n], // 返回的值, 这个可以随便控制
done: false // 为true就是结束, 为false就是继续
};
} else {
return {
done: true
};
}
}
};
};
result = [...obj];
console.log(result);
上面的方法可以满足我的要求了, 但是写法上真的不敢恭维, 代码量太多了..
所以我更推荐采用第二种方式利用Genertor
let obj = { '0': 'a', '1': 'b', length: 2 };
obj[Symbol.iterator] = function*() {
let n = -1, len = this.length;
while (len !== ++n) {
yield this[n];
}
};
result = [...obj];
console.log(result);
😺整个世界都清爽了.
三. defineProperty
这个神奇的属性做了很多很多神奇的事情, 可以说现在的前端如果不会用它的话真完全说不过去了...
功能
监控对象的某个属性, 可以对取值与赋值做出相应, 属于'元编程'
第一个参数是 监控对象
第二个参数是 key
第三个参数必须是一个对象, 也可以理解成config对象
比如 obj.name 这个会触发get函数
obj.name = 'lulu' 这个会触发set属性, 但是要注意, 这个不会触发get
这些动作都能够被监控到, 那我们就可以为所以为了哈哈哈哈哈
let obj = {
name: 'a'
};
function proxyObj(obj, name, val) {
Object.defineProperty(obj, name, {
enumerable: true, // 描述属性是否会出现在for in 或者 Object.keys()的遍历中
configurable: true, // 描述属性是否配置,以及可否删除
get() {
return val;
},
set(newVal) {
val = newVal;
}
});
}
proxyObj(obj,'name',obj['name'])
console.log((obj.name = 2));
缺点
- 在set里面没办法为自己赋值, 不然会导致死循环.
- 只能监控对象的变化, 无法监控数组与基本类型.
- 目标如果不是对象的话会报错...不知道为啥要设计成这样.
- Object身上的方法这个设定不太妥当, 下一章我们会聊聊Reflect.defineProperty.
四. 将$data代理到vm身上
当前本套工程里面, 使用data里面的数据需要this.$data.xxx, 我们把它变成this.xxx就可以直接访问的形式.
cc_vue/src/index.js
constructor(options) {
// 1: 不管你传啥, 我都放进来, 方便以后的扩展;
// ...
-------新加的
// 2: 把$data挂在vm身上, 用户可以直接this.xxx获取到值
this.proxyVm(this.$data);
-------新加的
// end
new Compiler(this.$el, this);
}
/**
* @method 把某个对象的值, 代理到目标对象上
* @param { data } 想要被代理的对象
* @param { target } 代理到谁身上
*/
proxyVm(data = {}, target = this) {
// 默认就挂在框架的实例上
for (let key in data) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newVal) {
if (newVal !== data[key]) {
data[key] = newVal;
}
}
});
}
}
这样以后再有访问数据的操作就可以直接this.了
之前对模板取值的操作要改一下啦, 很简单的就是去掉$data
cc_vue/src/CompileUtil.js
getVal(vm, expression) {
let result,
__whoToVar = '';
for (let i in vm.$data) {
// data下期做代理, 并且去掉原型上的属性
let item = vm.$data[i];
if (typeof item === 'function') {
__whoToVar += `function ${i}(...arg){return vm['${i}'].call(vm,...arg)}`;
} else {
__whoToVar += `let ${i}=vm['${i}'];`;
}
}
__whoToVar = `${__whoToVar}result=${expression}`;
eval(__whoToVar);
return result;
},
五. 为data所有数据添加劫持
这里比较核心, 所以我们直接单独抽离出一个'劫持模块'.
当前步骤只是添加了劫持, 关于具体劫持之后干什么, 请看下一条
cc_vue/src/Observer.js
class Observer {
constructor(data) {
// 我只是负责初始化
this.data = data;
this.observer(data);
}
/**
* @method 针对对象进行观察
* @param { data } 要观察的对象
*/
observer(data) {
// 循环拿出对象身上的所有值, 进行监控
if (data && typeof data === 'object'&& !Array.isArray(data)) {
for (let key in data) {
this.defineReactive(data, key, data[key]);
}
}
}
/**
* @method 进行双向绑定,每个值以后的操作动作,都会反应到这里.
* @param { obj } 要观察的对象
* @param { key } 要观察的对象
* @param { value } 要观察的对象
*/
defineReactive(obj, key, value) {
// 因为data数据可能会很深, 所以必须递归
this.observer(obj[key]);
let _this = this;
Object.defineProperty(obj, key, {
configurable: true, // 可改变可删除
enumerable: true, // 可枚举
get() {
return value;
},
set(newVal) {
if (value !== newVal) {
// 如果用户传进来的新值是个对象, 那就重新观察他
_this.observer(newVal);
value = newVal;
}
}
});
}
}
当然要在index里面启动这个模块
cc_vue/src/index.js
class C {
constructor(options) {
// 1: 不管你传啥, 我都放进来, 方便以后的扩展;
for (let key in options) {
this['$' + key] = options[key];
}
// 2: 劫持data上面的操作
new Observer(this.$data);
// ....
六. 添加Watch与Dep
'订阅发布'属于是vue比较核心的功能了, 这里也稍微有一点绕, 大家一起慢慢梳理.
现在data数据的改动已经被劫持, 思路梳理如下:
- 我要知道每个数据变化的时候, 我要做什么, 比如{{name}}, name变化的时候, 我要重新获取到name对应的值, 渲染到页面哪里?
- 比如一个变量有很多地方需要渲染我怎么办
cc_vue/src/Watch.js
发布订阅, 这个类很简单, 只是实现了两个功能, 让如队列与执行队列
export class Dep {
constructor() {
this.subs = []; // 把订阅者全部放在这里
}
/**
* @method 添加方法进订阅队列.
*/
addSub(w) {
this.subs.push(w);
}
/**
* @method 发布信息,通知所有订阅者.
*/
notify() {
this.subs.forEach(w => w.update());
}
}
cc_vue/src/Watch.js
观察者, 就是他稍微有点绕
export class Watcher {
// vm 实例
// expr 执行的表达式
// cb 回调函数, 也就是变量更新时执行的方法
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 这里取一下当前的value, 以后每次变化都对比一下oldvalue, 防止无用的更新
this.oldValue = this.getOld();
}
/**
* @method 只有第一次的取值会调用他,对老值的记录,以及被订阅.
*/
getOld() {
// 他只会被调用一次
// Dep是引用类型, 它身上的值当然可以传递
Dep.target = this; // 这个this指的就是watch自己
// 获取到这个值当前的value
let value = CompileUtil.getVal(this.vm, this.expr.trim());
// 操作完要制空
Dep.target = null;
// 给oldvalue赋值
return value;
}
/**
* @method 更新值.
*/
update() {
// 拿到新的value, 先比一比, 有变化再更新
let newVal = CompileUtil.getVal(this.vm, this.expr.trim());
if (newVal !== this.oldValue) {
this.cb();
}
}
}
Dep.target = this; 这句是点睛之笔, 我们来使用一下
cc_vue/src/CompileUtil.js
// 在解析模板的时候添加一个watch
text(node, expr, vm) {
let content = expr.replace(/\{\{(.+?)\}\}/g, ($0, $1) => {
// 因为模板只解析一次, 所以不用担心他被重复new
new Watcher(vm, $1, () => {
// 这里的callback就是具体的更新操作
this.updater.textUpdater(node, this.getContentValue(vm, expr));
});
return this.getVal(vm, $1);
});
this.updater.textUpdater(node, content);
},
getContentValue 获取元素内的所有文本信息
有的人会问为什么要把文本信息全更新, 而不是只获取变化的文本, 那是因为 很多时候我很会写出这样的代码 <p>{{a}}--{{b}}</p>, 那我们无法只单独改变b的样子, 因为我们操作的是 p标签的textContent属性
getContentValue(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, ($0, $1) => {
$1 = $1.trim();
return this.getVal(vm, $1);
});
},
上面的代码, 我们在解析text文本的时候放入了一个watch, 那么这个watch被new的一瞬间会执行getOldvalue方法, 那么就有了如下代码
cc_vue/src/Observer.js
defineReactive(obj, key, value) {
this.observer(obj[key]);
// 1: 劫持某一个值的时候, 创建一个dep实例
let dep = new Dep();
let _this = this;
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
// 2: 获取值的时候, 查看Dep这个类上面是否有target参数
// 这个参数是我们获取oldval时候挂上去的watch类
// 如果有的话, 调用把这个watch类放入订阅者里面
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newVal) {
if (value !== newVal) {
_this.observer(newVal);
value = newVal;
// 3: 每次更新数据, 都执行发布者
dep.notify()
}
}
});
}
其实想一想dep与watch也可以写成一个class, 但是写成两个更贴合设计模式.
实验
新建第二个文件夹, 专门用来检测双向数据绑定
cc_vue/use/2:双向绑定
<div id="app">
<p>n: {{n}} </p>
<p>n+m: {{n+m}} </p>
</div>
let vm = new C({
el: '#app',
data: {
n: 1,
m: 2
}
});
// 每秒给变一下n的值, n只要在屏幕上跟着发生变化就是成功了
setInterval(() => {
vm.n += 1;
}, 1000);
webpack方面配置调整一下
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../use/2:双向绑定/index.html'),
有兴趣的朋友可以试验一下我的工程的效果,
end
这次实现的只是初步的绑定操作.
下一集:
- 实现vue3.0的proxy模式的绑定, 我还没看vue3.0是怎么设计的, 先用自己的理解实现一下, 然后再学习他们的方法, 也是为了培养自己的思维.
- 篇幅足够的话会手写一个简易的axios, 方便测试代码.
大家都可以一起交流, 共同学习,共同进步, 早日实现自我价值!!
github:还没有star,期待您的支持
个人技术博客:个人博客
更多文章,ui库的编写文章列表 文章地址
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。