首页
Search
1
解决visual studio code (vscode)安装时没有选择安装路径问题
320 阅读
2
如何在 Clash for Windows 上配置服务
215 阅读
3
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
150 阅读
4
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
149 阅读
5
uniapp打包app提示通讯录权限问题,如何取消通讯录权限
113 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,190
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
753
篇与
的结果
2024-10-21
移动端里的逐帧动画
这几天在做一个感恩节的html5页面,得到了一个非常大的教训。页面地址: https://news.qq.com/FERD/ganen/main.htm页面里有比较多2,3帧的动画,我最初的设想是:针对2帧的动画,那么就用2张图交互的进行显示;针对3帧的动画,那么就3张图片轮流显示。在2帧动画里,一半时间显示,一半时间隐藏:样式:@keyframes shake2{ 0%{ opacity: 1; } 50%{ opacity: 1; } 51%{ opacity: 0; } 100%{ opacity: 0; } } .img1{ -webkit-animation: shake2 600ms linear infinite; animation: shake2 600ms linear infinite; } .img2{ -webkit-animation: shake2 600ms -300ms linear infinite; animation: shake2 600ms -300ms linear infinite; } 结构: 这种实现,在PC的模拟器上没有问题,移动端目前也没有问题。但是,如果当前页面中的图片非常多时,2帧动画就会出现问题。我们发现上面中的img1和img2会几乎同时的显示和隐藏,有一种闪屏的效果,如果图片较大,能亮下我的狗眼。在android手机里,动画更是简直没法看。刚开始是怀疑loading效果造成的,因为页面在loading完成之前,整个页面是隐藏的,会不会是animation在对隐藏的节点上出现问题呢。但是我也发现另一个现象,就是有的动画效果是在一张图片上完成的,比如扇子绕着扇子的根节点扇动,这里只需要为扇子添加一个transform:rotate的animation即可,这种动画就完全没有问题。后来在两张图片之前的思路上一直修修补补,但是完全没有左右。估计是图片的loading加载,导致animation的时间没有起作用,后来请教了一下同事,就把所有的2帧和3帧效果都用animation中的steps来实现。网上也有很多用animation-steps实现逐帧动画,在一张图片里,保存这种事物的不同形态,比如小人走路的不同状态,当切换这几种走路的状态时,小人就走起来了。如果没有使用steps,那么animation中的状态会以指定的动画慢慢变化;但steps是跳跃式动画,当前状态时间完成后会直接跳跃到下一个状态,没有过渡效果。语法:steps(number[, end | start]) 参数说明: number参数指定了时间函数中的间隔数量(必须是正整数) 第二个参数是可选的,可设值:start和end,表示在每个间隔的起点或是终点发生阶跃变化,如果忽略,默认是end。注意:第二个参数还有两个内置值,step-start等同于steps(1,start),动画分成1步,动画执行时以左侧端点为开始;step-end等同于steps(1,end):动画分成1步,动画执行时以结尾端点为开始。因此上面的2帧动画可以用steps实现,以下是用sass语法实现的逐帧动画,方便调用:@mixin keyframes($animationName, $width){ @-webkit-keyframes #{$animationName}{ 100%{ -webkit-transform: translate3d(-#{$width}, 0, 0); transform: translate3d(-#{$width}, 0, 0); } } @keyframes #{$animationName}{ 100%{ -webkit-transform: translate3d(-#{$width}, 0, 0); transform: translate3d(-#{$width}, 0, 0); } } } @mixin donghua($alltime: 0.9, $step: 2, $width:0){ $animation-name: unique-id(); // 生成唯一的animation name -webkit-animation: #{$animation-name} #{$alltime*1000}ms steps(#{$step}) infinite 0s; animation: #{$animation-name} #{$alltime*1000}ms steps(#{$step}) infinite 0s; @include keyframes(#{$animation-name}, $width); } 这时,动画在iOS和android都能正常展示了。欢迎访问: https://news.qq.com/FERD/ganen/main.htm 。参考: https://developer.mozilla.org/zh-CN/docs/Web/CSS/animation-timing-function
2024年10月21日
10 阅读
0 评论
0 点赞
2024-10-21
innerHTML对IScroll组件的影响
我们在移动端的页面中有时候会使用到IScroll的滚动组件,要么是应用到页面的整个区域,要么是其中的某个div。当我们已经对这块区域进行初始化后,再向页面中添加元素时会出现什么情况呢? 1. 向滚动区域中添加元素 # 一种情况是向滚动区域中添加元素,这个元素影响到了滚动内容的高度,比如这样的代码,.main是滚动区域的外壳:var myscroll = new IScroll('.main', { mouseWheel: true, scrollbars: true }); document.querySelector('.main .con').innerHTML += 'author: wenzi'; 在向页面中添加一个div元素后,如果不做任何改动,滚动条拉到底部是看不到这个新div元素的。只有使用refresh()方法,刷新滚动区域后,滚动条才重新计算:myscroll.refresh(); 2. 向滚动区域的父级元素中添加元素 # 还有一种情况就是向滚动区域的父级元素中添加元素,比如直接追加到元素body中:document.querySelector('.main .con').innerHTML += 'author: wenzi'; 当我们以innerHTML方式向body中追加元素后,出现了一个严重的问题,IScroll滚动区域直接卡死,无法滚动了。即使使用refresh()方法也不能奏效。那么是什么原因造成的呢?问题就出在innerHTML上,使用innerHTML为该元素添加html代码时,实际上是重写了该元素里所有的html代码,之前的代码全部被覆盖掉,才导致绑定在内部的IScroll事件失效,无法进行滚动了。在第一部分中,因为修改的是在IScroll滚动区域内部的内容,不会影响到IScroll的执行,所以添加的内容只是影响了滚动区域的高度,但滚动功能还是能正常使用的。我们可以再做一个实验: 使用innerHTML向.main元素的兄弟元素里添加或者修改元素,IScroll是完全没有影响的。 3. 解决方案 # 那使用什么方法向某个元素(比如body)中追加元素呢,答案是appendChild了,我们可以先用createElement创建一个div元素,然后把内部所有的内容都填充到这个div的内容,然后再在该元素上appendChild这个div元素:var div = document.createElement('div'); div.className = 'dialog'; div.innerHTML = 'author: wenzi'; document.querySelector('body').appendChild(div); 这种方式既不会对IScroll造成影响,也不用对IScroll的代码进行改动。方便作为第三方向某个成熟的系统中添加代码,不会对原系统产生影响。 4. 总结 # 关于innerHTML和appendChild,这里稍微总结下: - innerHTML appendChild 功能 重写该元素内所有的html 向该元素追加一个子元素 性能 高 低 事件 需添加到页面上才能绑定事件 创建后即可绑定事件 耦合度 高 低 关于使用哪种方式添加元素还得看这个项目的复杂程度。如果仅仅要实现一个简单的需求的话,用innerHTML性能还是好些。如果需要大规模重写html的话,可以使用模板引擎来完成。
2024年10月21日
6 阅读
0 评论
0 点赞
2024-10-21
再见2017,你好2018
类似的标题,不一样的心情。每年在写总结的时候,心里都是挺沉重的,原来才发现,这一年又白干了。活儿没少干,不过就是没挣到钱。“钱都去哪儿了”2017年里,确实发生了很多的事情,感觉比之前更焦虑了一些。简单的梳理下2018年里发生的事儿吧:5月初时,我们部门去内蒙草原见识了一番草原的风景。那天在去的路上,还飘着雪花,着实是一种别样的味道,到达草原时已经是晚上12点了,吃了羊肉大餐后,就去放烟花、篝火,然后去看星星。说实话,草原的5月份还是很冷的,晚上睡觉还要开电暖气和电热毯的。月底时,去了天津一趟,去那里办了点事情,很是匆忙,也没有去自己的逛逛。8月,忽然接到了家里的电话,爷爷不幸去世了。真是非常的突然,5月底回家时,还去奶奶家歇了会儿,爷爷的身体虽说不太好,但也没什么毛病,一顿饭也还能吃下2、3个馒头。不过爷爷就是一个闲不住的人,腿脚不灵便了还要地里转转,然后就再也没回来。真是那句话:“明天和意外,你永远不知道哪个先来”,岁月不饶人,在我们还没来得及尽孝心,他们就走了。父母在,不远游。载我们还有机会的时候,多尽一份孝心。9月时,部门有一次去深圳进行学术交流的机会,去聆听其他前端团队的技术经验,也见到了毕业好几年的大学同学。在听课的过程中,线上项目出了一些问题,课程没仔细听,净修改bug了。京东的双11从11月1号就开始了,除了买一些日常用品,还买了4本书:《你不知道的javascript(上)》,《俞敏洪》、《中国近代史》和《沈从文》。在之前一直说要去798转转,只是没有合适的机会,这次终于是陪着朋友去了一趟,说实话,真没什么看的,艺术氛围?我等俗人是感受不到。11月18时,大兴发生了一场大火,于是,浩浩荡荡的清理活动就开始了,所有的物流网点全部彻查,该关的都关了,本来在19大时,进京的物流就很慢了,这次的大火差点把北京的快递整瘫痪了。大兴也开始清理所谓的低端人口,而且,还不止是大兴,海淀、丰台等待那个区域都在清理,中介、房东也趁着这个机会把租金涨了一波,你爱租不组,现在市场上有的是人在找房子。后来北京又开始了大规模的拆广告牌活动,我们公司的牌子在拆了第2天后,海淀临时叫停了拆广告牌,什么时候恢复还得等待通知。当然,还有全国浩浩荡荡的煤改气、煤改电,为了所谓的环保政绩,简直是丧心病狂了。不管你怎么过冬、反正不能烧煤,煤改气工程也没完工、收你的烧煤炉子、贩煤的全部抓起来,河北、河南、山西等地全面车辆限号。12月份时,我们之前的中介被新中介昊园恒业收购了,这个中介比之前的中介还要黑,要么强制交满1年的房租,要么使用他的“元宝E家”;而这两种方案,选择哪个都不好,毕竟我们不会住到到期的,中间就要重新找房子。拖了一段时间到该交下次房租的时候,他们给出一个新的方案: 之前住的时间不算了,重新签1年,而且你可以压2付3的方式交房租,如果中间要转租的话,交半个月的转租费,同时新租户续租也签1年的合同。而且,之前交的卫生费泡汤了,这个新中介根本就没有保洁了。展望2018年,发现生活依然很艰难。生活虽然很艰难,但我们依然砥砺前行: 争取在2018年能升上一个职级; 努力挣钱,要努力; 前端的技术上能更上一层楼; 愿每个善良的人,都能被生活善待!
2024年10月21日
7 阅读
0 评论
0 点赞
2024-10-21
Vue与Git结合进行环境区分与自动化部署
因为上半年的事情确实多了点,很久没有写文章了。回到公司后没多久,病了一场,住院住了好多天。今天也是把之前项目里的经验拿出来给大家做一个参考,希望能有所借鉴。做的每个项目都要对自己多少有个突破,比如我之前做的“腾讯新闻-捐时长,做公益”的项目里,就第一次直接用 Vue 上手开始做,刚开始时也有点不熟悉,也是边做边查,最终也按照正常工期上线了。这里要讲的就是 Vue 里做开发,如何进行环境的区分。 1. 进行环境的区分 # 环境的区分主要是分为:开发环境、测试环境、预发布环境与正式环境。每个环境里的配置都是不一样的,比如首先是接口地址不一样,测试环境用的是测试接口,预发布环境和正式环境用的是正式接口;同时,每个环境从本地发布到发布系统时的流程也有些区别,比如往测试环境推送时,不用 md5 命名,推送文件即可,不用推送到 CDN 等等。这个项目我是用vue-cli生成脚手架的基础上进行开发的,在这个脚手架里,有一个全局变量process.env.NODE_ENV,代码里可以通过这个变量知道当前处于什么环境。process.env.NODE_ENV 规定了 3 个值: testing, development, production。在代码里就可以这么使用,根据不同的环境使用不同的接口:const apiOrigin = { production: window.location.protocol + '//api.xxx.com/', development: window.location.protocol + '//api.xxx.com/', testing: window.location.protocol + '//test.xxx.com/', location: '/', }; 那么怎么调用编译呢,一个最简单的方法是使用npm scripts,在 package.json 文件中配置好:{ "scripts": { "build:testing": "webpack config/webpack.testing.conf.js", "build:development": "webpack config/webpack.development.conf.js", "build": "webpack config/webpack.production.conf.js" } } 然后根据不同的操作,npm run不同的命令即可。可是你也发现了,有几个文件是没有的,需要自己配置。 2. 根据 Git 分支分别进行构建 # 我们早期没有自动的 CI/CD 构建发布流程,这里仅是根据 Git 的钩子来触发构建而已,然后顺带把源码放到 Git 服务上。我们的代码都是托管在 git 上的,那么可以根据不同的分支来表示不同的环境,通过下面的代码,能获取当前所在的分支:const { execSync } = require('child_process'); const fs = require('fs'); const async = require('async'); const str1 = execSync('git status', { encoding: 'utf8' }); const re = str1.match(/^On branch (.+)\s/); const branch = re[1]; console.log('当前分支为: ' + branch); 拿到分支后,再获取对应的配置即可。同时,为了更加简化我们的操作指令,可以利用 git 的钩子机制,将 npm script 操作写到钩子中,因为我们的内部系统,git 服务器我们是无法自定义的,因此只能在本地进行压缩和上传到访问服务器。这里我们使用的是pre-push钩子,在发出git push命令后,根据对应的分支进行相应的操作,操作完成后,再将源码推送到 git 服务器即可。访问服务器放置的是用户访问的 html 文件和静态资源,而 git 上只存放我们的源码。我们后续再根据 git 的原理进行分支管理和版本管理即可。【2022 年 2 月份补充】现在自动构建和发布流程已经很完善了,不用再单独通过上面的代码触发构建了,按照规范配置自动构建发布流程即可。
2024年10月21日
7 阅读
0 评论
0 点赞
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日
6 阅读
0 评论
0 点赞
1
...
53
54
55
...
151