首页
Search
1
解决visual studio code (vscode)安装时没有选择安装路径问题
138 阅读
2
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
131 阅读
3
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
107 阅读
4
如何在 Clash for Windows 上配置服务
77 阅读
5
Uniapp如何引入自定义样式文件
75 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,061
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
624
篇与
的结果
2024-10-21
仿Vue中的双向数据绑定实现
我们在使用Vue的过程中,双向绑定给了我们特别深的印象,data和view视图相互传递,只要其中一个发生了变化,另一个自动修改: {{username}} new Vue({ el: '#app', data:{ username: '' } }) 1. 快速的入门 # 1.1 Object.defineProperty # 在Vue中,是使用Object.defineProperty进行了数据劫持,当获取数据或者修改数据时,都能通过自定义的getter/setter截取到, defineProperty官方文档。我们先看一个简单的例子:var user = {name: 'wenzi'}, key = 'name'; // obj 传入的obj格式的数据 // key 要定义的key // val 当前key对应的值 function defineProperty(obj, key, val){ Object.defineProperty(obj, key, { enumerable: true, // 是否出现对象的枚举属性中 configurable: false, // get(){ // 获取key值时触发 console.log( 'get value: '+val ); return val; }, set(newVal){ // 给当前key设置值时触发 if(val!==newVal){ console.log(val+' --> '+newVal); val = newVal; } } }) } defineProperty(user, key, user[key]); // 为user中的key重新定义get/set方法 console.log( user.name ); // get value: wenzi user.name = 'skeeter'; // wenzi --> skeeter 可见在获取key对应的值时会触发get方法,在为key赋值时会触发set方法,那么我们在set方法里加入对应的修改视图的函数,就能在data发生变化时,视图同步更新。我们在写例子之前,先要考虑解决两个个问题: 如何获取视图DOM中对应相关的元素? 如何将属性值与视图DOM对应起来? 关于第1个问题,我们首先用最笨的方式来实现,然后再考虑优化。先拿到最顶端的元素,然后遍历所有的子元素,通过正则获取到制定规则的属性;对于第2个问题,我们先给每个属性一个回调函数,当对应的值发生变化时,启动这个回调函数。来看个例子 {{name}} {{age}} function Vue(options){ this.el = options.el; this.data = options.data; this.watcher = {}; // 将obj里所有的属性都进行监听 Object.keys(this.data).forEach(key=>{ this.defineProperty(this.data, key, this.data[key]); }); // 解析DOM this.compile(); } Vue.prototype = { defineProperty(obj, key, val){ let self = this; Object.defineProperty(obj, key, { enumerable: true, // 是否出现对象的枚举属性中 configurable: false, // get(){ // 获取key值时触发 console.log( 'get value: '+val ); return val; }, set(newVal){ // 给当前key设置值时触发 if(val!==newVal){ console.log(val+' --> '+newVal); val = newVal; // console.log( self ); // 拿出在watcher中保存的函数,然后更新 self.watcher[key](newVal); } } }) }, compile(){ // 解析DOM视图中双括号之间的内容 let ele = document.querySelector(this.el); [].slice.call(ele.childNodes).forEach(node=>{ let reg = /\{\{(.*)\}\}/, text = node.textContent; if( reg.test(text) ){ let key = reg.exec(text)[1]; // 获取到key node.textContent = this.data[key]; // data中存储的key值渲染到视图中 // 定义一个对应key的函数,在defineProperty中的set进行调用 this.watcher[key]=(newVal)=>{ node.textContent = newVal; } } }) } } let ww = new Vue({ el: '#app', data: { name: 'wenzi', age: 28 } }); // 2000ms后更新 setTimeout(()=>{ ww.data.name = 'skeetershi'; ww.data.age = Date.now(); }, 2000) 在我们定义的Vue的prototype里,定义了两个方法,defineProperty用来监听属性, compile用来解析DOM,然后通过watcher中定义的方法将两者关联起来。 1.2 data数据代理 # 是不是觉得在使用ww.data.name时很别扭,要是能直接通过ww.name访问和修改属性就好了。这里我们做一个小小的代理,把data里的属性都绑定到Vue上,修改时同步到data里:function Vue(options){ this.el = options.el; this.data = options.data; this.watcher = {}; // 将obj里所有的属性都进行监听 Object.keys(this.data).forEach(key=>{ this.proxyKeys(key); // 添加代理 this.defineProperty(this.data, key, this.data[key]); // 监听属性 }); // 解析DOM this.compile(); } Vue.prototype = { // 其他方法不变,添加一个代理方法,将data里的属性定义到this上,即Vue的实例中 proxyKeys(key){ let self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get(){ // console.log(self); return self.data[key]; }, set(newVal){ self.data[key] = newVal; } }) } } 这样通过实例ww就能直接访问和修改data里的数据了:// 2000ms后更新 setTimeout(()=>{ ww.name = 'skeetershi'; ww.age = Date.now(); }, 2000) 1.3 createDocumentFragment # 在上面例子的compile中,我们是直接用的DOM节点进行遍历与操作的,但是我们也知道,直接操作DOM有可能会引起页面的回流,影响页面的性能。DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(reflow)(对元素位置和几何上的计算)。因此,使用文档片段document fragments 通常会起到优化性能的作用(better performance)。documentFragment 被所有主流浏览器支持。所以,没有理由不用。compile(){ // 解析DOM视图中双括号之间的内容 let ele = document.querySelector(this.el); var fragment = document.createDocumentFragment(); var child = ele.firstChild; while (child) { // 将Dom元素移入fragment中 // appendChild: 如果被插入的节点已经存在于当前文档的文档树中, // 则那个节点会首先从原先的位置移除,然后再插入到新的位置. fragment.appendChild(child); child = ele.firstChild } [].slice.call(fragment.childNodes).forEach(node=>{ let reg = /\{\{(.*)\}\}/, text = node.textContent; if( reg.test(text) ){ let key = reg.exec(text)[1]; // 获取到key node.textContent = this.data[key]; // data中存储的key值渲染到视图中 // 定义一个对应key的函数,在defineProperty中的set进行调用 this.watcher[key]=(newVal)=>{ node.textContent = newVal; } } }) ele.appendChild(fragment); } 2. 实现原理 # 在第1部分的快速入门里,我们已经有一个简要的了解了,虽然上面没有实现从视图到数据的更新,不过也不难理解,就是给对应的DOM绑定上事件,反向更新。从数据到视图的更新,是需要对数据进行监听劫持,这里我们设置一个监听器Observer来实现对所有数据的监听; 设置一个订阅者Watcher,收到属性的变化通知并执行相应的函数,从而更新视图; 同时,要设置一个解析器Compiler,解析视图DOM中所有节点的指令,并将模板中的数据进行初始化,然后初始化对应的订阅器。流程图如下: 2.1 监听器Observer # 采用递归的方式,将所有的属性都进行监听,不过我们这里没有对后加入的属性进行处理。因此只能处理初始化时的属性:function Observer(data){ this.walk(data); } Observer.prototype = { walk(data){ if( !data || typeof data!=='object' ){ return false; } Object.keys(data).forEach(key=>{ this.defineProperty(data, key, data[key]); }) }, defineProperty(data, key, val){ this.walk(val); Object.defineProperty(data, key, { enumerable: true, // 是否出现对象的枚举属性中 configurable: false, // get(){ // 获取key值时触发 console.log( 'value: '+JSON.stringify(val) ); return val; }, set(newVal){ // 给当前key设置值时触发 if(val!==newVal){ console.log( 'new value: '+JSON.stringify(newVal) ); val = newVal; } } }) } } var user = { name: 'wenzi', age: 27, score:{ eng: 88, math: 90 } }; new Observer(user); user.age = 20; // new value: 20 user.score.math = 'qwerty'; // new value: "qwerty" 同时我们还得有个容器,来存储所有的订阅者,当数据发生变化时,来通知这些订阅者。这里就用到了发布者/订阅者模式,订阅者就是每个Watcher,发布者是每个数据的set函数,那么谁来执行发布这个动作呢,这就需要一个Dep队列,当数据发生变化时,通过Dep来通知所有的订阅者:function Dep(){ this.subs = []; } Dep.prototype = { addSub(watcher){ this.subs.push(watcher); }, notify(){ this.subs.forEach(watcher=>{ watcher.run(); }) } }; 那么什么时候执行add,什么时候执行notify呢? 执行notify的时机我们是没有疑问的,在set里;那么add呢?add方法在get里添加即可,理由: watcher是在什么时候用的,是在数据发生变化时更新视图的,在Watcher初始化时获取当前属性的值时进行触发,但是get里不能添加无意义的watcher,因此这里需要有一个限制条件,限制条件后面再说。Observer中的defineProperty现在变成了这样:defineProperty(data, key, val){ this.walk(val); let dep = new Dep(); // 定义队列 Object.defineProperty(data, key, { enumerable: true, // 是否出现对象的枚举属性中 configurable: false, // get(){ // 获取key值时触发 if(条件允许){ dep.addSub(watcher); } return val; }, set(newVal){ // 给当前key设置值时触发 if(val!==newVal){ val = newVal; dep.notify(); // 发布消息 } } }) } 2.2 订阅器Watcher # 订阅器Watcher在初始化时需要把自己添加到队列中,而且我们也在Observer中的get里执行了添加watcher的操作,所以这里我们只需要在Watcher初始化完成后,触发get操作即可,即获取对应的属性值,就能把wachter添加到队列中。同时,我们只有在Watcher初始化时才需要添加订阅者,get方法会被多次的调用,因此这里我们用Dep.target缓存下订阅者,添加完成后再删除掉:// vm Vue的实例 // exp data中的key // cb 回调函数 // options function Watcher(vm, exp, cb, options){ this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); // 获取key对应的值,同时将watcher添加到Dep的队列中 } Watcher.prototype = { get(){ Dep.target = this; let value = this.vm[this.exp]; Dep.target = null; return value; }, update(){ this.run(); }, run(){ let value = this.vm[this.exp]; // 新值 // 当执行run方法时,value值与之前保存的value值不同时 if( value!==this.value ){ let oldValue = this.value; this.value = value; this.cb.call(this.vm, value, oldValue); // 执行在初始化时添加的回调函数 } } }; 这时候我们可以把Observer中的defineProperty再次修改一下了:defineProperty(data, key, val){ this.walk(val); let dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: false, get(){ // Dep.target存储的就是Watcher实例 if(Dep.target){ dep.addSub(Dep.target); } return val; }, set(newVal){ if(val!==newVal){ val = newVal; dep.notify(); } } }) } 到目前为止,我们就能实现一个简单的从数据到视图的更新了。这里我们先不解析视图中的标签,仅仅是用一个写死的标签,后面我们再完善解析器Compiler。 {{name}} function Vue(options){ this.el = options.el; this.data = options.data; this.watcher = {}; // 将obj里所有的属性都进行监听 Object.keys(this.data).forEach(key=>{ this.proxyKeys(key); // 添加代理 }); new Observer(this.data); let exp = 'name', // 绑定key为name的只 ele = document.querySelector(this.el+' .'+exp); // console.log(ele); ele.innerHTML = this.data[exp]; // 将该值更新到视图中 // 为exp属性添加订阅者,回调函数里将最新的值更新到视图中 new Watcher(this, exp, value=>{ ele.innerHTML = value; }) } 具体完整的代码可以查看这个链接: github-index2代码在代码可以看到,我们在Vue中创建了一个订阅者,这个订阅者的回调函数里,把exp对应的最新值给了ele.innerHTML,这样在name值更新时,会马上更新到视图中。 2.3 解析器Compiler # 在上面的代码里,我们没有使用解析器,而是直接获取视图中的元素。不过实际中我们还是要用解析器去解析视图中的指令的。解析器的作用一方面是解析出视图中相关的指令,将数据填充到视图中,另一方面也是添加订阅器,在数据发生更新时,能同步更新到视图中Vue中的命令非常多,而且也做了很多的兼容,比如双括号中可以进行运算,使用过滤器等。我们这里就简单的解析几个,比如有: v-model, {{}}, v-on。解析的过程主要分为以下几个步骤: 把真实DOM元素转换为文档片段; 遍历文档片段中所有的节点,解析出双括号指令和v-*的指令; 根据解析出的不同的指令,执行不同的操作; v-model: 初始化数据,添加订阅器,同时添加input事件 v-text: 初始化数据,添加订阅器 v-on: 为当前node节点添加对应的事件和回调 function Compiler(vm, el){ this.vm = vm; this.el = document.querySelector(el); this.fragment = null; this.init(); } Compiler.prototype = { init(){ if (this.el) { this.fragment = this.nodeToFragment(this.el); // 文档片段 this.compileElement(this.fragment); // 解析 this.el.appendChild(this.fragment); // 将文档片段添加到视图中 } else { console.log('Dom元素不存在'); } } } 在解析指令的过程中,会频繁的操作DOM,为了防止影响页面的性能,我们首先把DOM元素转换为文档片段:// 将DOM节点转为文档片段 nodeToFragment(el){ var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 将Dom元素移入fragment中 // appendChild: 如果被插入的节点已经存在于当前文档的文档树中 // 则那个节点会首先从原先的位置移除,然后再插入到新的位置. fragment.appendChild(child); child = el.firstChild } return fragment; } 然后我们就是解析文档片段中的指令,这里我们先解析双括号{{}}的指令:compileElement(fragment){ var childNodes = fragment.childNodes; [].slice.call(childNodes).forEach(node=>{ var reg = /\{\{(.*)\}\}/; // 双括号 var text = node.textContent; // 节点中的文本值 if (this.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令 // reg.exec(text)[1] 解析出双括号中的属性名 this.compileText(node, reg.exec(text)[1]); } // 继续递归遍历当前节点的子节点 if (node.childNodes && node.childNodes.length) { this.compileElement(node); } }); }, // node 当前的文档节点 // exp 节点中{{}}的属性名 compileText(node, exp) { let val = this.vm[exp]; // 获取到在Vue中data已定义的属性 node.textContent = val; // 将初始化的数据初始化到视图中 // console.log( exp ); new Watcher(this.vm, exp, value=>{ // 生成订阅器并绑定更新函数 node.textContent = value; }); }, isTextNode(node) { return node.nodeType == 3; } 在上面的代码中,我们解析到当前是文本节点且含有双括号后,则将data中对应的属性值更新到视图中,同时添加订阅器,当数据发生变化时则更新到视图中。在解析v-on的事件指令,v-on的完整指令是v-on:event="fn",因此我们先从DOM中所有的节点里先筛选出**v-***的属性,然后再细分是对应的那个指令。在compileElement的forEach中:compileElement(fragment){ var childNodes = fragment.childNodes; [].slice.call(childNodes).forEach(node=>{ var reg = /\{\{(.*)\}\}/; // 双括号 var text = node.textContent; // 节点中的文本值 if (this.isElementNode(node)) { // 当前节点为元素节点 // 解析当前元素节点中的attr属性 v-* this.compileAttr(node); } else if (this.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令 // reg.exec(text)[1] 解析出双括号中的属性名 this.compileText(node, reg.exec(text)[1]); } // 继续递归遍历当前节点的子节点 if (node.childNodes && node.childNodes.length) { this.compileElement(node); } }); }, compileAttr(node){ var nodeAttrs = node.attributes; // 获取当前节点上所有的属性 var self = this; // 对这些属性进行遍历,如果是带有v-的属性则再进行细分 [].slice.call(nodeAttrs).forEach(attr => { // console.dir(attr); let attr_name = attr.name; if (/v\-/.test(attr_name)) { // v-* 的指令 let exp = attr.value; let dir = attr_name.substring(2); // 获取v-*后面*的具体值 if (/on\:/.test(dir)) { // 当前为v-on:event类型 // 绑定事件 self.compileEvent(node, self.vm, exp, dir); } else { // v-model self.compileModel(node, self.vm, exp, dir) } // 操作完成后删除当前v-*的属性 node.removeAttribute(attr_name); } }) }, // 添加对应的事件,并绑定回调函数 compileEvent(node, vm, exp, dir) { let eventType = dir.split(':')[1]; let cb = vm.methods && vm.methods[exp]; if (eventType && cb) { node.addEventListener(eventType, cb.bind(vm), false) } else { console.error('Vue no method: ' + exp); } }, isElementNode(node){ return node.nodeType === 1; } 这样在Vue中的methods里定义对应的事件后,在视图里使用on就能绑定上事件: new Vue({ el: '#app', methods:{ getNowTime(){ return Date.now() } } }) 我们还有一个v-model需要解析,v-model是要把数据同步到视图中,同时在input标签的内容发生变化时反向更新数据。因此我们在初始化视图,添加订阅者的同时,还要添加一个input事件(针对input[type=text]标签):// 为model绑定input事件 compileModel(node, vm, exp, dir) { let self = this; let val = this.vm[exp]; node.value = val; new Watcher(this.vm, exp, value => { node.value = value; }); // 添加input标签 node.addEventListener('input', e => { let newVal = e.target.value; if (val == newVal) { return false; } self.vm[exp] = newVal; val = newVal; }, false); } 最后我们初始化一个Vue实例,大功告成,最终版的代码可以点击git-最终代码: {{username}} {{timestamp}} let app = new Vue({ el: '#app', data: { username: 'wenzi', timestamp: '', }, methods:{ getNowTime: function(){ this.timestamp = (new Date()).toLocaleTimeString(); } } }); 3. 总结 # 在学习Vue的双向绑定过程中,经常被Observer, Watcher和Compiler之间的关联关系搞混,在通过反复的写代码,梳理他们的关系后,终于搞清楚了。当然,目前也只是实现了基本的功能,更多的功能还需要后续再阅读源码进行完善。
2024年10月21日
4 阅读
0 评论
0 点赞
2024-10-21
CSS中margin出现空白的问题
我们写了这么多的CSS,也多少了解了margin的一些特性,比如: 水平边距永远不会重合。 垂直边距可能在特定的框之间重合: 常规流向中两个或多个块框相邻的垂直边距会重合。结果的边距宽度是相邻边距宽度中较大的值。如果出现负边距,则在最大的正边距中减去绝对值最大的负边距。如果没有正边距,则从零中减去绝对值最大的负边距。 在一个浮动框和其它框之间的垂直边距不重合。 “绝对定位的框”与“相对定位的框”边距不重合。 不过这里我们要说的当前元素的margin与其父级元素之间产生的效果。我们以这个DOM结构来举例,.container为最外层的父级元素:*{margin: 0; padding: 0;} .container{background-color: #d8bea5;} .user{background: #8bce92;} .user .b1{margin-top: 20px;} 123 我们先看下上边距的情况,在 .user, .a1, .b1中,无论哪个元素,只要其父级元素没有设置padding-top或者没有其他元素(元素节点或者文本)的输出,那么当前元素的顶部会与其父级元素的顶部对齐,margin-top的边距就会上升到其父级;然后一直向上寻找,直到body元素。比如上面的结构中,我们给.b1一个margin-top值,最终会上边距会给到了.container如果我们只想给当前元素一个人margin-top值,并不想影响其父级元素怎么办呢? 给其直接父元素一个不为零的padding-top值; 给当前元素的前面添加一个高度不为零的兄弟元素,这个兄弟元素可以用visibility:hidden占位隐藏,但不能用display:none隐藏,因为dispplay:none会使这个元素的高度变为0; 给父元素一个透明的边线: border:1px solid transparent; 将父元素设置为绝对定位 position:absolute; 父元素添加 overflow:hidden; 最后就是考虑是否将margin-top切换成padding-top。 再一个就是下边距的问题,下边距会与上边距产生一模一样的问题,也可以按照这3种方式来解决。不过这里的下边距还有另外一个问题。我们使用这条语句,将.container的最小高度设置为页面的高度:function resize(){ document.querySelector('.container').style.minHeight= document.documentElement.clientHeight + "px"; } resize(); 我们再给.user或.a1或.b1一个margin-bottom值,我们会发现,在.user的高度+下边距还远远没有超过.container的高度时,页面就已经产生滚动条了,说明margin-bottom已经作用到.container了。之前没注意到这个现象,是我们的产品在用之前的页面修改后作为新页面的时候,发现产生了滚动条,调试后才发现,原来是里面的一个元素有margin-bottom把整个页面撑开了,造成页面产生了滚动条,底部有一部分的白底。这个怎么解决呢,解决方法依然同上!
2024年10月21日
13 阅读
0 评论
0 点赞
2024-10-21
Vue中对数组特殊的操作
1. 为什么 Vue 中不能通过索引来修改数组以更新视图 # 我们知道在 Vue 中的数据是通过Object.defineProperty这种劫持的方式来实现数据更新的,可是数组是一个比较特殊的类型。官网上说: 由于 JavaScript 的限制,Vue 不能检测以下变动的数组: 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue 当你修改数组的长度时,例如:vm.items.length = newLength 我搜寻其他的文章里也说是因为大部分浏览器Object.observe()支持的不友好,不能检测很好的检测到数组的数据变化,因此在 Vue 中则使用了其他的方法来实现数据的更新。那么我就在想,作者为什么不用 Object.defineProperty 来实现对数组的数据劫持呢,不能实现的原因是什么呢?我们这里来看个 demo:function defArray(obj, key, val) { Object.defineProperty(obj, key, { get() { console.log('get val: ' + val); return val; }, set(newVal) { console.log('old value: ' + val); console.log('new value: ' + newVal); val = newVal; }, }); } let arr = [0, 1, 2, 3, 4, 5]; arr.forEach((item, index) => { defArray(arr, index, item); }); arr[1] = 123; // 修改索引为1的值 /* > arr[1] = 1 old value: 1 new value: 123 123 */ console.log(arr[2]); // 获取索引为1的数据 /* > console.log(arr[2]); get val: 2 2 */ arr 数组的长度为 6,修改索引在 0-5 之间任意索引的数据,都能触发 set 方法,获取相应的数据时,就能触发 get 方法。不过当获取额外的数据时,或者先删除原数据再添加时,就不会触发相应的方法了:arr[6] = 6; console.log(arr[6]); // 6 delete arr[2]; arr[2] = 2; console.log(arr[2]); 因为上面我们只对已存在的索引的数据进行数据劫持,新添加的数据或者先删除再添加的数据,都是没有 get 和 set 方法的,都不能通过数据劫持的方式来更新视图。但数组是正常更新的,只是无法更新视图了。所以我在这里大胆的猜测一下作者不用索引来更新视图的原因:防止因新添加或者先删除再添加等的操作,导致后续添加的数据没有 get 和 set 方法,在 Vue 中就统一操作,无论是数组还是 Object 类型的数据,都可以通过Vue.$set(Array|Object, key, val)来操作数据,并更新视图(当然,Object 类型的数据,在 Vue 初始化时就已经存在的 key,是可以直接通过 key 来修改的哈)。作者尤雨溪在 GitHub 上有专门的回复,为什么 vue 没有提供对数组属性的监听: 因为性能问题,性能代价和获得的用户体验收益不成正比。 以下源码均摘自与 github 上的Vue.js v2.5.17-beta.0版本。 commit 链接: Vue.js v2.5.17-beta.0 2. 变异方法更新数组 # 在官方网站中,提供了以下几个方法来检测数组的变化,也会触发视图的更新: push() pop() shift() unshift() splice() sort() reverse() 在 Vue 的源码中,对 Vue 中的数组数据实现了类似的方法,这里官方的叫法是变异方法,比如push()方法,并不是调用 Array 中原生的 push 方法,而是调用自己实现的 push 方法,来实现数据的劫持,再通过 Array 中的 push 方法实现数组数据的更新,可能有点晕,看代码,我这里把几个距离较远的代码都放到一块了,但代码都没动,只是改了位置:// 获取原生Array中提供的所有方法 var arrayProto = Array.prototype; // 将原生提供的方法创建一个新的对象,以免修改原生的方法,造成全局污染 var arrayMethods = Object.create(arrayProto); // 将要实现的几个变异方法 var methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; /** * Intercept mutating methods and emit events * 截取这些方法,然后实现相应的操作 */ methodsToPatch.forEach(function (method) { // cache original method var original = arrayProto[method]; // 缓存原始方法 // def即Object.defineProperty的包装函数 def(arrayMethods, method, function mutator() { var args = [], len = arguments.length; while (len--) args[len] = arguments[len]; var result = original.apply(this, args); // 调用原始方法完成对数组的更新 var ob = this.__ob__; var inserted; // 存储要修改的数据 switch (method) { case 'push': case 'unshift': inserted = args; break; case 'splice': inserted = args.slice(2); break; } console.log(args, inserted); if (inserted) { ob.observeArray(inserted); } // 如果有修改的数据,则添加observer监听器 // notify change ob.dep.notify(); // 触发更新 return result; }); }); /** * Observe a list of Array items. * 对数组中的每项添加监听器 */ Observer.prototype.observeArray = function observeArray(items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } }; 这里我们也就知道了,在 Vue 中的数组数据,调用上面的那些方法,都是事先实现的同名方法,然后再通过apply调用原生方法,再添加监听器,触发更新等后续操作。如果我们自己想单独实现下这样的操作如何实现呢,如果把仅仅把上面的代码拿出来是不行的,这些自定义的变异方法还没有跟数组进行绑定呢,数组调用这些方法时,还是原生的方法,因此这里我们需要把这些变异方法绑定到数组上,我们来看下 Vue 源码中的实现:// can we use __proto__? var hasProto = '__proto__' in {}; // __proto__是否可用 /** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */ var Observer = function Observer(value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { // 当前数据为数组,若__protp__可用,则调用protoAugment方法,否则调用copyAugment方法 var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); // 监听数组的每一项 } else { this.walk(value); } }; /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ * 把这些方法定义到原型链__proto__上 */ function protoAugment(target, src, keys) { /* eslint-disable no-proto */ target.__proto__ = src; /* eslint-enable no-proto */ } /** * Augment an target Object or Array by defining * hidden properties. * ie9、ie10 和 opera 没有 __proto__ 属性,为数组添加自定义方法 */ /* istanbul ignore next */ function copyAugment(target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; def(target, key, src[key]); } } 这样就能给每个数组添加上这些变异方法了。因此 Vue 中也可以使用splice()来更新数组中的内容:vm.$arr.splice(index, 1, newVal); 我们自己实现这样的操作时,可以把上面的 Observer 简化一下:function init(value) { // can we use __proto__? var hasProto = '__proto__' in {}; var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); } var arr = [1, 2, 3, 4, 5]; init(arr); arr.splice(2, 1, 2); arr.push(6, 7, 8); 3. Vue.$set 的实现 # 不多说话,直接看源码怎么实现的,内部有本人的中文注释:/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. * 在一个对象上设置一个属性。如果该属性尚不存在,则添加新属性并触发更改通知。 */ function set(target, key, val) { if ('development' !== 'production' && (isUndef(target) || isPrimitive(target))) { warn('Cannot set reactive property on undefined, null, or primitive value: ' + target); } // 如果当前对象是数组,且key是合法的数字索引 if (Array.isArray(target) && isValidArrayIndex(key)) { // 若索引值小于数组的长度,则数组长度不变,只修改 // 若索引值比当前数组的长度大,则说明是新添加元素,需要更新数组的长度 // 这里采用变异方法splice来更新数组 target.length = Math.max(target.length, key); target.splice(key, 1, val); return val; } // 以下操作时,target均为object对象类型 // 若当前key已经存在于对象中,且没有原型链中,直接更新即可 // 因为在Vue初始化时已经添加了监听器,这里就不用再添加了 if (key in target && !(key in Object.prototype)) { target[key] = val; return val; } var ob = target.__ob__; // 若当前对象是Vue实例,则不允许使用这种方式添加属性,应当在初始化时预先声明 if (target._isVue || (ob && ob.vmCount)) { 'development' !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.', ); return val; } // ? 若当前对象没有监听器?则直接赋值?这里不太能理解 if (!ob) { target[key] = val; return val; } // 若当前key不存在,则在ob.value上创建set和get方法,并触发更新 defineReactive(ob.value, key, val); ob.dep.notify(); return val; } 因此使用 Vue.$set 方法更新数组时,内部依然是用的splice()方法来操作的,而更新对象类型的数据则略微复杂点。本人学艺不精、才疏学全,如果还有什么疏漏的地方,还望批评指正!
2024年10月21日
5 阅读
0 评论
0 点赞
2024-10-21
如何在npm上发布你的package
在自己平时工作或者学习中,总结出来的模块,我们可以将其提炼发布到npm上,这样即使有多个项目使用这个包时,也能方便地进行更新。我在把自己之前总结的模块发布到npm上时,遇到了几个问题,现在把刚才踩过的坑亮出来,方便大家能随时躲过去。 1. 使用rollup而不是webpack # 在我的模块里,只是使用几个简单的ES6语法,而且最终编译出来的代码要能在ES6, AMD, CMD中均能使用。针对这个简单的功能,就没必要使用webpack这么大的工具了,使用rollup即可,而且打包出来的额外代码还特别少。rollup的配置也很简单,我们先看下package.json引用的模块:{ "name": "tencent-downapp", "version": "0.0.4", "description": "集成了微信、QQ、腾讯视频、QQ音乐和腾讯动漫等APP的api,用来打开和下载第三方的api", "main": "dist/bundle.js", "repository": "https://github.com/wenzi0github/tencent-downapp.git", "files": [ "src", "dist/bundle.js" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "rollup -c" }, "author": "wenzi", "license": "ISC", "devDependencies": { "babel-core": "^6.26.3", "babel-loader": "^7.1.4", "babel-plugin-external-helpers": "^6.22.0", "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.7.0", "rollup-plugin-babel": "^3.0.5", "rollup-plugin-node-resolve": "^3.3.0" } } rollup.config.js的配置内容:import resolve from 'rollup-plugin-node-resolve'; import babel from 'rollup-plugin-babel'; const config = require( './package' ); const banner = '/*!\n' + ' * AppDownload v' + config.version + '\n' + ' * last update: ' + (new Date()).toLocaleDateString() + ', author: skeetershi\n' + ' * Released under the MIT License.\n' + ' */' export default { entry: './src/index.js', plugins: [ resolve(), babel({ exclude: './node_modules/**', plugins: ['external-helpers'] }) ], output: { file: './dist/bundle.js', format: 'umd' }, banner }; 这样就行了,执行npm run build后,就能在dist目录中看到编译后的文件。 2. 发布时忽略的内容 # 发布到npm上的包,默认是不上传node_modules文件夹中的内容的。同时呢,你还可以创建一个.npmignore文件,来确定更多的忽略文件和文件夹。 如果你的模块里还有.gitignore文件时,同样也会忽略这里面包含的文件和文件夹。所以,通常编译后产生的目录(我这里的是dist目录),我们是不加入git追踪的,把dist目录添加到.gitignore中。可是这样也就上传不到npm上了,我们就需要在package.json中,添加一个files数组字段:{ "files":[ "dist" ] } 这样就能把dist目录中所有的文件内容都发布到npm上。当然,这里也可以写成单独的文件:{ "files": [ "dist/bundle.js", "dist/bundle.min.js", "dist/bundle.esm.js" ] } 3. 引用时,引用的是哪个文件 # 刚开始我有这样的一个疑惑,模块里这么多文件,当install完成后,require或者import文件夹时,用的是哪个文件呢,这里就需要在package.json中指定好即可:{ "main": "dist/bundle.js" }
2024年10月21日
4 阅读
0 评论
0 点赞
2024-10-21
单页面应用中js获取url中的参数
在很久之前写过一篇文章【javascript获取URL链接和js链接中的参数】,在那篇文章里我们讲解了如何直接获取当前页面链接中的参数,和修改后获取任意url的参数。不过在最近的需求里,之前文章里的知识已经无法满足了。现在我们是基于Vue开发的单页面应用,单页面应用里是使用#hash进行路由切换的,那么现在的问题是: 无论之前的URL有没有带参数,都会在最后加上#。比如从其他链接跳转过来时带有参数,页面的URL就会变成这样:http://www.xxx.com/?a=1&b=2#/ 如果从主页再往二级页跳转时还带有参数,页面的URL就会成这样:http://www.xxx.com/?a=1&b=2#/user?uname=蚊子&year=2018 在这个具有两个?的链接,location.search获取到的是哪个值?测试一下我们就会发现,获取到的是前面?到#之前的参数:console.log( location.search ); // "?a=1&b=2" 因此这里我们需要对整个url进行检索,这样hash#前后的数据就都能获取到了:function getQueryString(name) { var reg = new RegExp("[?&]" + name + "=([^]*)", "i"); var res = window.location.href.match(reg); if( res && res.length>1 ){ return decodeURIComponent(res[1]); } return ''; } getQueryString('a'); // "1" getQueryString('b'); // "2" getQueryString('uname'); // "蚊子" getQueryString('year'); // "2018" 到这里这个功能已经完工了。不过在我们项目的具体实施中,还有一个特殊的需求: 有的参数会以json string格式放在某个字段里传递进来,然后页面再获取到这个字段后进行JSON.parse转换,最后才能获取到json中的值。之前是每次getQueryString('extra')获取到数据后,再进行json解析,而且为了防止json解析错误,还都要加上try...catch。因此,我也把这个特殊的需求融入到了这个函数里,过程也非常简单,就是多了一步额外的检测:// name: 要获取的字段 // extra json string存放的字段 function getQueryString(name, extra='extra') { var get = function(_name){ var reg = new RegExp("[?&]" + _name + "=([^]*)", "i"); var res = window.location.href.match(reg); if( res && res.length>1 ){ return decodeURIComponent(res[1]); } return ''; } var res = get(name); // 若存在参数则直接返回 if( res ){ return res; } // 若不存在,则检测需要的参数是否包含在extra中 let _addparams = get(extra); if( _addparams ){ try{ let info = JSON.parse(_addparams); return info[name]; }catch(e){ } } return ''; } 这样在项目的其他地方直接使用即可,不用再考虑name字段具体在哪个位置。
2024年10月21日
7 阅读
0 评论
0 点赞
1
...
28
29
30
...
125