本次任务 html
不少文章都写过他们两个的区别前端
可是这些区别只是表面上的, 我来展现一个有趣的👇vue
<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...
经过上面的问题我有了些思考node
综上所述, 仍是用forEach保险!
可是就想用map怎么办那?
1: slice的原理就是一个一个的循环放入一个新数组;webpack
let slice = Array.prototype.slice; slice.call(oD.childNodes).map(()=>{})
2: 扩展运算符原理不太同样, 但他同样能够把全部元素都拿出来, 接下来咱们就对他进行死磕.ios
[...oD.childNodes].map(()=>{})
这个神奇的语法其实有不少门道的, 咱们来一块儿死磕一下吧.
下面代码会正确执行, 对象放入对象确定没问题git
let obj = {a:1,b:2}, result = {...obj}; console.log(result)
下面代码会报错, 由于缺乏iterable.github
let obj = {a:1,b:2,length:2}, result = [...obj]; console.log(result)
缘由是'扩展运算符'不知道该怎么扩展他, 咱们要告诉如何扩展才能够正确的执行.
Symbol.iterator是Symbol身上的属性, 而iterable的key就是它.web
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);
上面的方法能够知足个人要求了, 可是写法上真的不敢恭维, 代码量太多了..
因此我更推荐采用第二种方式利用Genertorexpress
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);
😺整个世界都清爽了.
这个神奇的属性作了不少不少神奇的事情, 能够说如今的前端若是不会用它的话真彻底说不过去了...
功能
监控对象的某个属性, 能够对取值与赋值作出相应, 属于'元编程'
第一个参数是 监控对象
第二个参数是 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));
缺点
当前本套工程里面, 使用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; },
这里比较核心, 因此咱们直接单独抽离出一个'劫持模块'.
当前步骤只是添加了劫持, 关于具体劫持以后干什么, 请看下一条
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); // ....
'订阅发布'属因而vue比较核心的功能了, 这里也稍微有一点绕, 你们一块儿慢慢梳理.
如今data数据的改动已经被劫持, 思路梳理以下:
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'),
有兴趣的朋友能够试验一下个人工程的效果,
此次实现的只是初步的绑定操做.
下一集:
你们均可以一块儿交流, 共同窗习,共同进步, 早日实现自我价值!!
github:尚未star,期待您的支持
我的技术博客:我的博客
更多文章,ui库的编写文章列表 文章地址