一位北京阿里的朋友M君到杭州出差,前几天一块吃饭,15-16年间,我们都在猎豹移动,是一个大组的同事兼饭友。聊了很多事情,一些共同认识的人和事,各自的行业动态等。过去几年,我对业内几家公司的判断,都准了,并在非常初期的时候就发现了他们,也极力推荐身边的朋友,关注这些公司的工作机会,然而我们都没能抓住。包括:
15年,看好字节跳动(今日头条);
16年,看好快手;
17年下半年,看好拼多多;
18年,知乎付费,个人机会来临,有待验证
随后发了条朋友圈,200多个点赞,30多条留言,比如这样的:
京杭君神算子
佩服!这样的洞察力&远见,很好奇是怎么做到的?
请问下京杭君是怎么做出判断的?
……
大家似乎对我这套认知体系很感兴趣。
我是怎么做出这些判断的呢?
下笔之前,潜意识想起了曾经看过的一部电影和一篇演讲。
电影《贫民窟的百万富翁》,2009年在中国大陆放映。10年过去,剧情已经比较模糊,大致记得,出身贫民窟的男主,参加一档电视有奖竞猜节目,奇迹般地答对了所有问题,一度引起警察的怀疑。实际情况是,他过去生活经历里,某些印象深刻的片段,碰巧跟这些问题相关。
另一个是2005年,乔布斯在年斯坦福大学毕业典礼的演讲。整篇演讲,乔布斯讲了三个故事:The first story is about connecting the dots.
第一个故事是生命中的点点滴滴串连起来。
这个故事的最后,乔布斯是这样总结的:
再次说明下,你不可能将未来的片断串连起来;你只能在回顾的时候将点点滴滴串连起来。所以你必须相信这些片断会以某种方式在未来的某一天串连起来。你必须要相信某些东西:你的勇气、命运、生命、因缘,随便是什么。这种方法从来没有令我失望(let me down),只是让我的生命更加地与众不同。
乔布斯的演讲里,这一段至今印象深刻,因为我时常有这种感觉。
下面就讲讲我生活经历中跟上面那些判断相关的片段。这些片段在以前的文章提到过,这里重新整理,更加详细的细节,也可以翻翻文末的旧文。
老读者都知道,京杭君出身湖南贫困农村,父母农民,下面有两个弟弟,一路村里的小学,镇上的初中,市里的高中。第一次高考离一本线差10分,各种原因在家里呆了一年,一度千夫所指。然后又机缘巧合去县里的一所排名靠后的高中补习,05年二志愿上了西安一所普通的211,毕业后到北邮读研究生,然后在三星电子进入职场。
在三星的困境和突破探索
研三开始在三星实习,200块一天,这笔钱对当时的我非常重要。尽管早就锁定了三星的正式Offer,入职后时薪也是实习的两倍以上。但是整个研三都在那里上班,因为太需要这笔钱,放弃了去了解其他类型公司的机会。
人穷志短,贫穷真的会限制你的思维和格局,出不来还不自知。
所以年轻的朋友,京杭君以过来人的身份告诉你们:贫穷是一种病,症状之一目光短浅。当你一无所有、迷茫不知道干什么的时候,使劲琢磨赚钱吧,不会错的,还能顺带治好懒病、游戏病、自卑或自妄、激发好奇心……打通认知的任督二脉,京杭君亲测有效。
京杭君在三星电子正式上班3年,第一年发生了两件事情对我影响很大。
没能解决北京户口。原本以为自己参加实习的时间早,工作上上司认可,解决北京户口应该没什么悬念,因为往届的研究生都解决了。我们那届招人多,当年公司的户口指标不到招聘应届生人数的一半,HR分配户口的方式是抽签,我没有抽到。
这件事情对我的打击,跟第一次高考失利差不多,感觉天塌了,眼前一片灰暗,茫然不知所措。我的留京计划被打乱,用了几个月才缓过神来。现在看来,可笑至极,人生不就是一个缓慢受锤的过程么。
了解了公司的晋升和薪酬制度。我的mentor是一位同校师兄,他06年毕业,毕业时起薪跟6年后我毕业差不多,而房价在6年间涨了三四倍,师兄毕业一年后(之前实习一年),家里稍微帮了点,20万左右首付在回龙观买了120平的房子。
而我,家里帮不了忙,急需反哺,房价却在继续上涨,如果继续在这里按部就班,以这里升职加薪的速度,5年内在北京买房实属无解。
落户失败已成事实,扎根北京之路注定会比那些已经落户的同学要艰难,但仍未放弃。工作之余开始思考如何改善自己的经济状况,因为相对加速增长的房价和日渐见长的年龄,留给我的时日不多了。到底能不能留下来,就看这几年的造化。
穷则思变,我必须做点什么,来改善收入状况。当时是这么思考的。
业余兼职做外包本质上和打工是同一种模式,出卖时间,一份时间卖一次,没有积累可言,身体不适就不能进行,长远看缺乏可持续性,天花板低。
投资理财是当代社会人必须学会的一项技能,越往后对财富积累影响越大,能带来「睡后」收入,自带时间复利,长远看非常值得花精力去研究。
股市入场门口低,不受场地制约。利用业余时间即可,跟上班不冲突。受过高等教育,研究、逻辑能力得到过系统训练,理论上具有可行性。
研究股票,能锻炼搜集,辨别,挖掘信息的能力,思维能得到锻炼,视野也能进一步打开,反过来对本质工作和职业发展也有正面的协同作用。我这人本来就充满好奇心,喜欢研究各种烂七八糟的东西,这一点非常契合我的性格。
进可攻退可守,做不好还有工作兜底,万一做好了……
于是着手调研,混迹「雪球财经」、「Seeking Alpha」等当时比较流行的股票资讯网站或社区。也读了几本有关股票投资方面的书籍,其中《彼得林奇的成功投资》,有一句至今印象深刻。
普通人因为自己生活或工作经历,比专业人士能更早获得一手市场讯息。
这话怎么理解?举个例子,一位爱逛街,喜欢买买买的女性,哪家店的衣服,品质佳,款式潮,卖的好,人流量如何,是不是又扩张开新店了。只要留心,可以第一时间感受这些变化,这里面可能就存在股票投资的机会。如果你是某家店的一线雇员,那就更了解这家店的一手市场信息。
我自己在科技行业工作,全球最好的科技公司都在美国上市,再加A股各种消息市政策市乌烟瘴气等诟病,自然而然就选择了美股。
13年六七月份,还清了家里债务,稍微余钱。当时苹果手机已经非常流行了,忘了在哪里读过一篇文章,说「特斯拉」电动车是汽车界的苹果手机。研究了一周,激动不已,直接加速了我的开户进程。
八月份开好户,找同学亲戚凑了10万(输的起),发现「特斯拉」的时候,股价80美元左右,等开完户涨到110多,机会不等人。巴菲特说过,好公司好价格才值得买入,嫌贵,一直没有买。
当时在三星电子,从事手机研发工作,其时小米势头迅猛,但是小米没有上市,买不到它的股票。作为一个股市菜鸟,非常容易被那些曾经的十倍股神话激励,总想穷尽一切可能的方法,去挖掘未来的潜在十倍股。
传统科技公司大部分过了增长期,体量大而稳定,股价波动小。移动互联网应用正值爆发期,融资神话此起彼伏,注意力自然而然落在了互联网公司上面。
花了很多时间学习、研究这类公司各个维度的信息。创始人经历和现状、高管团队、主要产品、商业模式、市占率、护城河、行业地位、潜在增长空间、竞品等不一而足。买过40美金的Facebook,50的奇虎360,150左右的百度,时间飞快,账户涨涨跌跌,转眼到了13年底,增值一倍。
行情好的时候遍地股神,跟现在A股的情况差不多,感慨股市真是知识变现的好战场,胜利带来过分的自信。找堂哥从老家民间借贷20万,年化利息12%,14年初追加投入股市,股票账户市值40万人民币。幻想以13年的业绩水平,两年内即可北京买房,财富自由亦指日可待,一切岁月静好。
现实是半年后(14年中),账户只剩下20万RMB,除去13年盈利,还亏了10万本金。加之美股晚上(北京时间)开盘,养成了盯盘的坏习惯,晚上醒来第一件事是瞄一眼手机股票行情,对健康亦造成不良影响。
一时主业空间受限,副业遭受挫折。本来经济基础不牢,亏钱心里更虚。反思和复盘,股市投资是一项长期事业,不可操之过急,风控第一,先应该考虑的是如何不亏钱。调整计划,必须先把主业做好,根基牢固了,再谈副业,否则本末倒置,缘木求鱼。
发现字节跳动
当时毕业2年多,马上30,能不能留北京,就看这2-3年的造化。算上实习时间,已经在三星电子呆了3年,因工作关系,跟通信产业链上下游同行有过不少接触,大家对行业前景看法普遍悲观。加之通信是一个大玩家的游戏,作为受雇个体,求职选择有限,跟热火朝天的互联网行业天壤之别。
所以当时就计划转行,互联网行业薪资标准比通信行业高,前景也更好,本来也是程序员,转过去有基础。另一个原因是,当年(14年)阿里巴巴上市,研究生时期对面宿舍有位同学毕业就去了阿里,他在上市前晋升一级到P6,授予的股票按上市当天的收盘价算,值100多万。这事对我启发很大。
二级市场挖掘十倍股,标的是上市公司,一般体量都不小了。未上市的优质公司,要分享他的增长果实,最好的方式就是加入它,并持有它的期权。什么样的公司值得加入,我有一套自己的标准,在以前的文章里也有提到过,当时是这么总结的。
公司所从事的行业有想象空间,已经或者有潜力构造护城河。
公司短期不会死掉,未上市,估值不高,创始人有分享利益的意愿。
创始人有过创业经历,能力得到过证明,大概率确保公司能做大。
换句话说:公司有做大的可能,目前还很小,老板人品靠谱,跟员工慷慨分享利益(大方发期权),有能力把公司做到上市,员工有可能据此财务自由。字节跳动跟这个标准完美匹配。
那么字节跳动(今日头条)又是如何进入我视野的呢?
我对这家公司的初始印象,来自两次新闻事件(我做股票后一直关注行业动态)。
2014年06月,C轮融资1亿美金、估值5亿美金
随后被搜狐起诉
成立2年,估值5亿美金,成长不可谓不快,融到钱,短期死不了。被老牌门户起诉,说明头条实力不可小觑,引起了老玩家警惕。
另外因为我自己的工作经历,对张一鸣也有所耳闻。前面不是说我在三星电子做手机开发吗,我们每年都参与多个型号手机的开发工作,公司的抽屉里一堆工程机。
每一款旗舰机型,我们都会第一时间试用。过程中,我发现一个奇怪的现象:每一款三星的手机,里面是都会预装一大堆App。有运营商的,也有其他公司的,运营商的App可以理解,毕竟三星跟各大运营商紧密合作。但是为什么要预装其他各种非运营商或手机厂商自己的App呢?这些App大部分都是垃圾,没什么卵用,非常影响使用体验。我每试用一款手机,第一件事是卸掉里面绝大部分预装App,有一些还因为权限问题卸不掉。
带着这个问题,稍微深挖,这里面是一条利益链,当时市面上的安卓手机厂商还很多,竞争同质化白热化,绝大部分手机厂商不赚钱,手机出厂前预装App,是第三方公司推广自己App一个有效手段,是需要给手机厂商付费的,都是生意。
也有听闻张一鸣召集深圳中小手机厂商,合作预装字节跳动各款App的传闻。最开始,一个预装成本才几毛钱,真便宜啊,不得不佩服张一鸣的眼光,多年的摸爬滚打经验不是盖的。现在听说某些App的安装成本已经几十块了。
以上说明,我虽然在通信行业,但对字节跳动这家公司是有所了解的。当时最想加入的公司名单里,还有小米、滴滴、美团。但是后面这些公司体量都不小了。
另外巧合的是, 跟京杭君同一年入职三星电子,有一位校友L。15年初有一次下班,乘坐同一部电梯下楼,闲聊得知L的对象D君在字节跳动工作,据她说当时今日头条整个团队300多人,大喜,于是通过L认识了D君,加上了微信。
然后去字节跳动的官网查找招聘岗位,并选定了推荐算法工程师,按岗位要求,查漏补缺,有的放矢。
15年4月初从三星离职后,请D君内推,不久就安排了面试,一个下午,在知春路盈都大厦,2个技术面通过。HR面试后告知一周后还有终面,在HR的介绍下,第一次见到了D君。D君是同届校友,毕业后在一家二梯队互联网公司工作一年后,于13年下半年加入字节跳动。跟D君聊了10分钟左右告别,D告知头条已经700多号人了,技术人员100号左右,终面基本上不会刷人。一周后技术VP Y君终面,遗憾未有通过。
这是跟头条的第一次接触,据说当时头条估值20亿美金左右。
15年5月份拿到了另一家初创公司,商汤科技的Offer,因为更看好资讯流市场和字节跳动这间公司,最终拒掉了Offer。大概一年后,京杭君同一个导师下面的研究生同学,从一家名牌外企跳到了商汤。商汤这几年发展非常迅猛,目前已经是计算机视觉领域的领头羊,这是后话。
15年6月份,我去了猎豹移动,做猎豹浏览器上的新闻推荐。之所以选择猎豹,是觉得新闻推荐这个方向市场需求比较大,前景看好,以后还有机会去今日头条碰碰运气。
在猎豹移动,进一步确定了字节跳动这间公司的潜力,因为他们千人千面的个性化推荐模式,已经成了行业标准,各个新闻APP都在学习他们。
于是推荐身边的熟人关注字节跳动的工作机会,比如说百度一位同学和师弟,也跟猎豹的同事分享了自己跟字节跳动的有关经历和看法。大家的反馈都差不都。
今日头条App太Low了,没见身边有人用……
在猎豹呆了一年后,各种原因要离职,请D君再次推荐头条的推荐算法岗位,最后简历卡在技术VP Y君那里,HR不能安排面试。
在猎豹离职前拿到百度手百App Feed推荐策略岗位Offer。6月初通过某职场社交App,头条效率工程部的一位开发工程师C君找到我,怂恿过去聊一聊,我告知之前跟头条相关故事。从C君了解到,他们部门老大是从百度贴吧来的一位经理H君,Y管不到H部门。于是过去,当时今日头条搬到了中航广场, 大楼比较气派,跟C君面聊,这个组主要做一些内部效率工具平台,跟我的技能储备不匹配,亦无太多兴趣,也就没有进行后续的面试。
17年初,有个之前认识的HR入职头条,联系京杭君,说有个岗位非常匹配,问愿不愿意过去聊聊,京杭君对进入头条已不抱希望,跟hr表示,希望过去之前跟技术负责人先电话沟通一下。
后来跟技术负责人S电话沟通,通过某职场社交App了解到S在百度阿里待过几年,14年进入头条。S表示他是某产品线推荐算法负责人,他们正在做一款针对高端人群版的快手。京杭君把之前的故事告知S君,S君表示他先去找Y君了解一下情况,当然最终也没有面试安排。
所谓针对高端人群的快手,就是现在的网红级短视频应用「抖音」。
这是京杭君跟头条的第三次接触,头条在2016年年末融资后估值110亿美金。
乔布斯说:你不可能将未来的片断串连起来,你只能在回顾的时候将点点滴滴串连起来。所以你必须相信这些片断会以某种方式在未来的某一天串连起来。
我因本职工作空间受限,急迫想改善自己的经济状况,玩起了美股。急不可耐地去挖掘十倍股,形成了一套自己的方法论,然后圈定了互联网行业,股市虽然亏了,但为后面的转行提前两年打下了认知基础。因为本职工作手机开发,对App预装有所了解,又因为同事兼校友引荐,找到了帮忙内推的人。虽然最后功亏一篑没有坐上火箭,但也进入了资讯流行业,和推荐算法这个岗位。这中间,每个点都是独立的,在某一天,连点成线。
以下是字节跳动几次主要融资的时间和估值数据,我没有看错,其增长速度远超我的预期。
2013年,6千万美元
2014年,5亿美元,一年间增长7.3倍
2016年,110亿美元,两年间,增长21倍
2017年,220亿美元,一年间,增长1倍
2018年,750亿美元,一年间,增长2.4倍
17到18年的增长,主要贡献来着抖音。
从2013、2014、2016、2017年到2018年最后一轮融资,估值分别增长了1249倍、149倍、5.8倍、2.4倍。5年间增长1249倍,历史罕见。
另一方面,字节跳动的造血能力很强,15年就能实现盈亏平衡,16-18年广告营收如下。
2016年,60亿元
2017年,150亿元
2018年,500亿—550亿元
2019年,预期营收超过1000亿元
要知道,直到18年,百度这间创立了18年的公司,营业收入才正式突破1000亿元。而字节跳动这间公司成立至今,不过7年多时间,真是一间不差钱的公司。
对了,有位同学,我从15年开始,劝他看字节跳动工作机会,他一直不看好,甚至刚开始还说字节跳动太Low,在18年初入职头条,没多久,头条新一轮估值,他的期权涨了2倍多。
发现快手
我在猎豹移动呆了不到一年,资讯流业务没做起来,有迹象表明,老板要放弃这块业务。我们团队被隔壁团队兼并,原来的上司带了一票人离开,我也没有得到新上司的信任。哪怕是隔壁团队,也有更大的老板带了一帮亲信另投他主。
我也该走了,尝试头条还是未果。既然去不了心仪的快速增长的创业公司,那就先去大长锻炼一下业务能力,为以后抓住这样的机会积攒能量。
国内互联网公司做算法哪家强,在我的认知里,无疑是百度凤巢,于是,从某职场社交App上联系了一位百度凤巢的技术经理,请其帮忙内部推荐,该经理告知:“凤巢暂未开放招聘职位,但可以推荐其他岗位”,就这样,简历推到了手百Feed流项目组。
当时头条的内容流广告已经引起的百度的重视,百度内部从战略高度重视内容,首当其冲为手百Feed。两周内做了5场面试,除了最后一场定级委员会面试定级低于预期,其他都顺利。就这样去了百度。
拿到百度的口头Offer,离开猎豹之前,通过社交网络认识了一位隔壁县老乡Z君,Z君年长京杭君几岁,清华研究生毕业后,在外企工作过几年,然后跟同学创业,也是做APP的,小有成就。
Z君很随和,我把自己工作上的情况跟他简单说了下。他说在咱们住得不远,周末有空出来聊聊。我非常珍惜这种机会,层次比你高的业内前辈,还愿意指导你,太难得了。
于是主动约了周末,在北苑华贸城请Z君吃饭。Z君拉来了另外一为老乡S君,他们同一个县,S君跟京杭君同一年硕士毕业,也是清华的,在小米做信息流推荐工作。在我谈到后面工作选择时,Z君建议看看快手,增长很猛,S君也说从小米的平台看,它的数据很漂亮。
快手CEO 宿华清华毕业,也是湖南人,做快手之前折腾过好几个项目,曾经有一段时间,跟这位年长几岁老乡的公司合租一个办公场地。16年路过五道口多次,见过楼顶上大大的Logo,所以对这家公司有印象。
在猎豹的时候,同大组有一位中科院的同事兼朋友,这位朋友比京杭君晚入职,在他入职之前,我们就已经认识了。我听他说过,他有位师弟H君,从猎豹离职去了快手。
通过朋友加上了他师弟H君微信,H君说当时快手员工总共100号左右,加班比较严重。他在工程组,不认识推荐部门的人。我继续调研,用我之前十倍股的方法论来分析快手的方方面面。各项数据都表明,它已经进入了快速增长期。
上它的官网找招聘信息,没找到推荐算法类的岗位。于是又从某职场社交App找到了CEO助理,直接沟通,之后不了了了。
昨天晚上一位老乡联系我,让我留意有没有合适他的工作机会。我想到了一位快手的HR朋友,快手还在大规模招人,为老乡牵了线。这位朋友也是我以前所在公司的HR,正是他安排了我的面试和后续入职流程。
他离职后我们还保持着联系。职场上,应该都是这样的,公司里负责招聘你的HR,面试过你的同事,上司和团队里帮助过你的人,总是会对他们怀有感激之情,不管离职与否。某种程度上正是因为他们,你才得以进入公司,获得一份工作。
顺便跟HR朋友聊到了16年找快手CEO助理X姐毛遂自荐一事,他说当时X姐当时可能是太忙,忘记了这件事,你应该Push一下她的。我又看了一眼的X姐的头衔,已经是副总裁了。
快手这边没反馈,百度那边催着入职,我就去百度了。同样,推荐身边的关系比较近的朋友关注快手的工作机会。当然反馈也跟15年推荐头条差不多:
快手太Low,没见身边有人用快手。
百度这边是Feed流项目组,16年公司级的战略项目。业务增长很快,团队扩充的速度也很快,在百度的前半年,除了做好本职工作,我利用一切业余时间,学习我感兴趣的项目和技术,百度在这方面有深的积累。
16年年末,我了解到我在17春季晋升季没有晋升资格,因为规定要求在原岗位呆满一年才有晋升资格,下一次要等到17年秋季,加北京上房价疯涨,我觉得必须去杭州上车了。17年4月份,找到一份杭州大厂的工作机会。
还是4月份某天,快手的一位HR小哥通过某职场社交软件找到我,说有一个岗位非常合适我,问我有没有兴趣。我答应第二天下去过去看看。反正要去杭州,过去看看对自己没啥坏处。两个技术面,第三面是HR小妹,她说面试结果不太好。
我问了一些面试官相关的信息,她告知,一面官是北大毕业的硕士,在阿里妈妈工作几年后过来的。二面官是L君(后面进一步挖掘得知,L君在百度凤巢呆过,跟快手CEO宿华一个组,后来宿华离职创业,几经折腾,宿华加入快手出任CEO,L君也成了快手的前10号员工之一,当时是推荐算法的负责人),当时快手400多人了。
回去的路上,我觉得好像有什么不对,到公司一查,原来这正是我一年前关注过,还推荐身边熟人关注的公司。如果记起了这些,我一定会认真准备一翻,再去面试。我刚去百度时业务太忙,以及过度关注技术方面的提升,后半年又关注杭州这边的工作机会。把快手这家公司彻底忘了。
我觉得必须补救一下,不能再错过快手的机会。推荐部门那边面试失败,广告部门可以再试一试,推荐和广告技术上80%是相通的,但我不认识广告那边的人。
我需要找一个朋友引荐,这样成功的概率高一些。在百度工作期间,认识了《计算广告》的二作W君,我们小团队开始做小视频推荐业务,后面业务调整,短视频业务交给W君团队,我负责协同过滤召回模块,交接串讲的时候,W君坐在旁边,问了很多问题。
事后才知道他是《计算广告》的二作。W君在计算广告届也算是知名人物,我问他快手广告部门有没有熟人。他说认识Q君,Q君我是有所耳闻的。请W君帮忙引荐,他没有答应,原因不明,也许有他自己的难处。W君问我愿不愿意去他团队,他团队很缺人,那时我已经打算离开百度,去意已决。
我告诉W君我对快手的思考和看法,正值快速增长期,期权有很大的升值空间,很定比呆在百度强,建议他看看那边的机会。W君说自己刚升T8,不可能离开百度的,我也就没有多说了(去年从百度老同事得知,W君升T9了,真快呀,3年多点时间从T7升到T9,对了,W君好像86年的)。
Q君是13年中科院毕业的硕士,读书期间在百度实习,参加数据挖掘类的比赛,毕业时以阿里星的身份去了阿里巴巴,定级P6,比普通研究生高一级,做商品推荐算法相关的工作,3年时间升到P8,16年下半年去了快手。
京杭君碰巧认识Q君在中科院读书时,参加数据挖掘比赛的队友X君,X君当时是他们队长,他们都是中科院同一级不同所的同学。16年初,X君从某公司跳槽美团,负责某业务的广告投放技术工作。他组建自己的团队,从某职场社交APP上联系上了还在猎豹移动的京杭君,问有没有兴趣过去聊聊,我婉拒了好意。
于是向X君说明意图,问他是否可以引荐Q君,X君欣然答应,Q君要了简历,说马上安排面试。我把之前去快手推荐部门面试的情况跟他说明,Q君说这种情况,系统内部有记录,不能再安排面试了。
快手尝试失败,我觉得没有遗憾了,安心去杭州。离开北京之前,再次跟几个要好的朋友推荐快手,把我跟快手的接触以及看法全盘告诉他们,其中包括最近来杭州出差的M君,17年5月我来了杭州,7月原来猎豹的老同事P君入职快手,18年快手再次融资是,P君的期权升值了5倍,P君正是我去猎豹面试的二面官。对了,Q君最新的Title是商业化部VP。
行文至此,篇幅已经很长了,后续会总结以下话题。
如何形成自己的趋势感知体系?
职场要诀:把握时机
职场要诀:跟对人
作者简介:岳京杭,湖南人、码农。求学于西安、北京,目前定居杭州。浪迹于通信、互联网行业。曾就职于三星电子、百度、网易等公司。沉于阅读,勤于思考,乐于分享。复盘过往经历、面向未来思考、做最优决策。
公众号:中产之路
声明:本文系作者投稿,如需转载请联系原作者。
热 文 推 荐
☞大学生利用漏洞“骗走”京东110万, 中心化白条的漏洞, 区块链能否补得上?
☞1/10个iPhone Xs = 英伟达最便宜AI计算机,这是唯一的“核弹”?
System.out.println("点个在看吧!");
console.log("点个在看吧!");
print("点个在看吧!");
printf("点个在看吧!\n");
cout << "点个在看吧!" << endl;
Console.WriteLine("点个在看吧!");
Response.Write("点个在看吧!");
alert("点个在看吧!")
echo "点个在看吧!"




每日更新至更新完毕,建议关注收藏点赞。
学习顺序
快速上手->实战->API->迁移指南(这篇在单独笔记中点击跳转)->Vite
工具推荐
IDE配置: Visual Studio Code + Vue - Official 扩展+es6-string-html 扩展(在字符串前加上一个前缀注释/*html*/
以高亮语法)+浏览器扩展vue.js devtools+…
创建应用
在本地搭建 Vue 单页应用。创建的项目将使用基于 Vite 的构建设置,并允许我们使用 Vue 的单文件组件 (SFC)。
- 单页面应用 (SPA)
一些应用在前端需要具有丰富的交互性、较深的会话和复杂的状态逻辑。构建这类应用的最佳方法是使用这样一种架构:
Vue 不仅控制整个页面,还负责处理抓取新数据,并在无需重新加载的前提下处理页面切换。这种类型的应用通常称为单页应用 (Single-Page application,缩写为 SPA)。 - 单文件组件 (即 *.vue 文件,英文 Single-File Component,简称 SFC) 是一种特殊的文件格式,使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中。Vue 的单文件组件是网页开发中 HTML、CSS 和 JavaScript 三种语言经典组合的自然延伸。< template>、< script> 和 < style> 三个块在同一个文件中封装、组合了组件的视图、逻辑和样式。
- 安装(构建步骤)
npm create vue@latest # Vue 官方的项目脚手架工具
#如果不确定是否要开启某个功能,你可以直接按下回车键选择 No。
cd <your-project-name>
npm install
npm run dev
npm run build #当你准备将应用发布到生产环境时
#此命令会在 ./dist 文件夹中为你的应用创建一个生产环境的构建版本。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
生成的项目中的示例组件使用的是组合式 API 和 < script setup>,而非选项式 API。
- 通过CDN使用Vue + 使用【全局/ES模块/导入映射表】构建版本
这个就无需下载,直接通过链接获取到vue包。可以用于增强静态的 HTML 或与后端框架集成。但是,无法使用单文件组件 (SFC) 语法。
这里我们使用了 unpkg,但你也可以使用任何提供 npm 包服务的 CDN,例如 jsdelivr 或 cdnjs。
//使用全局构建版本Vue
/*该版本的所有顶层 API 都以属性的形式暴露在了全局的 Vue 对象上。
这里有一个使用全局构建版本的例子*/
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const message = ref('Hello vue!')
return {
message
}
}
}).mount('#app')
</script>
//使用 ES 模块构建版本
<div id="app">{{ message }}</div>
<script type="module">//使用了
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
//注意全局构建和ES模块构建的vue地址不一样
createApp({
setup() {
const message = ref('Hello Vue!')
return {
message
}
}
}).mount('#app')
</script>
//导入映射表 Import maps
//导入映射表是一个相对较新的浏览器功能。请确保使用其支持范围内的浏览器。
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp, ref } from 'vue' //这时可以直接用'vue'
createApp({
setup() {
const message = ref('Hello Vue!')
return {
message
}
}
}).mount('#app')
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 许多关于组合式 API 的例子将使用 < script setup> 语法,这需要构建工具。如果你打算在没有构建步骤的情况下使用组合式 API,请参考 setup() 选项的用法。这个在API章节补充。
- 如果你打算在生产中通过 CDN 使用 Vue,请务必查看生产环境部署指南,这个在后续生产部署里会提及。
- 拆分模块->模块化
<div id="app">div>
<script type="module">
import { createApp } from 'vue'
import MyComponent from './my-component.js'
createApp(MyComponent).mount('#app')
script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
// my-component.js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `Count is: {{ count }}`
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
注意:由于安全原因,ES 模块只能通过 http:// 协议工作,也即是浏览器在打开网页时使用的协议。为了使 ES 模块在我们的本地机器上工作,我们需要使用本地的 HTTP 服务器,通过 http:// 协议来打开项目,否则会报错(因为 ES 模块不能通过 file:// 协议工作)。
要启动一个本地的 HTTP 服务器,请先安装 Node.js,然后通过命令行在 HTML 文件所在文件夹下运行 npx serve。你也可以使用其他任何可以基于正确的 MIME 类型服务静态文件的 HTTP 服务器。
基础篇
创建一个 Vue 应用
每个 Vue 应用都是通过 createApp 函数创建一个新的应用实例.
传入到createApp 中的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。如果你使用的是单文件组件,我们可以直接从另一个文件中导入根组件。
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
//例子:单文件组件
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)//App传入到createApp,作为根组件
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
应用都是由一棵嵌套的、可重用的组件树组成的。例如,一个待办事项 (Todos) 应用的组件树可能是这样的
App (root component)
├─ TodoList
│ └─ TodoItem
│ ├─ TodoDeleteButton
│ └─ TodoEditButton
└─ TodoFooter
├─ TodoClearButton
└─ TodoStatistics
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 挂载应用
应用实例必须在调用了 .mount() 方法后才会渲染出来。
该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串。
应用根组件内容会被渲染在这个容器中,容器并不属于这个应用。
app.mount('#app') //选择html里id为app的元素
//返回根组件实例而非应用实例。
- 1
- 2
这里区分一下“根组件”实例 v.s. “应用”实例
应用实例(app)是“整个应用的控制器”,而根组件实例(root component instance)是你传入的第一个组件的“运行副本”。
应用实例 是 createApp() 创建出来的整个 Vue 应用的控制器
根组件实例 是 App.vue 这个组件被“挂载”后生成的实际运行实例
特性 | 应用实例(app) | 根组件实例(App.vue) |
---|---|---|
创建方式 | createApp(App) | app.mount(‘#app’) 后产生 |
类型 | App 对象 | ComponentInternalInstance |
用途 | 全局配置、插件注入 | 页面响应式状态、逻辑处理 |
能访问 DOM 吗 | ❌ 否 | ✅ 是 |
能注册全局组件吗 | ✅ 是 | ❌ 否 |
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App) // 👈 应用实例
app.mount('#app') // 👈 会生成根组件实例(App.vue 的运行实例)
//容易搞混的场景
// ❌ 想访问组件内状态
const app = createApp(App)
console.log(app.count) // undefined ❌,app 是应用实例
// App.vue
setup() {//应在组件内部访问
const count = ref(0)
return { count }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 根组件模版
当根组件没有设置 template 选项时,Vue 将自动使用容器的 innerHTML 作为模板。这种DOM 内模板通常用于无构建步骤的 Vue 应用程序。它们也可以与服务器端框架一起使用,其中根模板可能是由服务器动态生成的。
<div id="app">
<p>{{ message }}p>
div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js">script>
<script>
const app = Vue.createApp({
data() {
return {
message: 'Hello from DOM template!'
}
}
})
app.mount('#app')
script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 应用配置
确保在挂载应用实例之前完成所有应用配置!
应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误;应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件;
app.config.errorHandler = (err) => {
/* 处理错误 */
}
app.component('TodoDeleteButton', TodoDeleteButton)//全局组件组册
//使得 TodoDeleteButton 在应用的任何地方都是可用的。
- 1
- 2
- 3
- 4
- 5
- 多个应用实例
应用实例并不只限于一个。createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
如果你正在使用 Vue 来增强服务端渲染 HTML,并且只想要 Vue 去控制一个大型页面中特殊的一小部分,应避免将一个单独的 Vue 应用实例挂载到整个页面上,而是应该拆开创建多个小的应用实例,将它们分别挂载到所需的元素上去。
const app1 = createApp({
/* ... */
})
app1.mount('#container-1')
const app2 = createApp({
/* ... */
})
app2.mount('#container-2')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
模版语法
Vue 使用一种【基于 HTML 的模板语法】,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。
在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。
如果你对虚拟 DOM 的概念比较熟悉,并且偏好直接使用 JavaScript,你也可以结合可选的【 JSX 】支持直接手写渲染函数而不采用模板。但请注意,这将不会有和模板同等级别的编译时优化。
- 文本插值 使用的是“Mustache胡子”语法 (即双大括号)
双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新。
<span>Message: {{ msg }}</span>
//v-html 插入HTML 而不是解析成纯文本
<p>Using text interpolation: {{ rawHtml }}</p>//纯文本
<p>Using v-html directive: <span v-html="rawHtml"></span></p>//html
//在当前组件实例上,将此元素的 innerHTML 与 rawHtml 属性保持同步。
- 1
- 2
- 3
- 4
- 5
- 6
声明式渲染也是这个东西。
- 使用 JavaScript 表达式
Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式,这些表达式都会被作为 JavaScript ,以当前组件实例为作用域解析执行。- 在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:
在文本插值中 (双大括号)
在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中
- 在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`">div>
<time :title="toTitleDate(date)" :datetime="date">
{{ formatDate(date) }}
time>
{{ var a = 1 }}
{{ if (ok) { return message } }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 全局访问
没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性。你可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用。
"显式包含"就是你要在模板中访问某个变量,它必须明确地:
出现在 setup() 返回对象中
或是注册在全局对象 app.config.globalProperties 上
或是 Vue 编译器内置允许使用的(如 Math)
指令汇总
指令由 v- 作为前缀,一个指令的任务是在其值变化时响应式地更新 DOM。
<p v-if="seen">Now you see mep>
<a :href="url"> ... a>
<a @click="doSomething"> ... a>
<a :[attributeName]="url"> ... a>
<a :['foo' + bar]="value"> ... a>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,避免在名称中使用大写字母,因为浏览器会强制将其转换为小写。如果你的组件拥有 “someAttr” 属性而非 “someattr”,这段代码将不会工作。单文件组件内的模板不受此限制。
- v-html attribute 被称为一个指令。
注意,你不能使用 v-html 来拼接组合模板,因为 Vue 不是一个基于字符串的模板引擎。在使用 Vue 时,应当使用组件作为 UI 重用和组合的基本单元。
Vue 不是基于字符串拼接的模板引擎,而是基于组件的响应式视图框架。
<template>
<div v-html="dynamicTemplate">div>
template>
<script setup>
const dynamicTemplate = '' // ❌ 无效
script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
慎用v-html:在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。
- v-bind
下面例子中v-bind 指令指示 Vue 将元素的id attribute 与组件的 dynamicId 属性保持一致。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。
<div v-bind:id="dynamicId">div>
<div :id="dynamicId">div>
<div :id>div>
- 1
- 2
- 3
- 4
- 5
布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。
<button :disabled="isButtonDisabled">Buttonbutton>
- 1
动态绑定多个值
//如果你有包含多个 attribute 的 JavaScript 对象:
const objectOfAttrs = {
id: 'container',
class: 'wrapper',
style: 'background-color:green'
}
//通过【不带参数】的 v-bind,你可以将它们绑定到单个元素上
<div v-bind="objectOfAttrs"></div>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
响应式基础
能在改变时触发更新的状态被称作是响应式的。
场景 | 推荐写法 |
---|---|
需要单个值 | 用 ref() |
需要对象或多个属性 | 用 reactive() |
模板中用 ref | 自动解包,无需 .value |
模板中用 reactive | 用 .属性 访问 |
脚本中使用 ref | 需要 .value |
由 reactive() 创建的对象都是 JavaScript Proxy,其行为与普通对象一样,reactive() 只适用于对象 (包括数组和内置类型,如 Map 和 Set)。
而另一个 API ref() 则可以接受任何值类型。Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map。ref 会返回一个带有 .value 属性的 ref 对象,并在 .value 属性下暴露内部值。
import { ref } from 'vue'
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
//要在组件模板中访问 ref,请从组件的 setup() 函数中声明并返回它们:
import { ref } from 'vue'
export default {
// `setup` 是一个特殊的钩子,专门用于组合式 API。
setup() {
const count = ref(0)
// 将 ref 暴露给模板
return {
count
}
}
}
//模版.html
<div>{{ count }}</div>
//注意,在模板中使用 ref 时,不需要附加 .value。
//当在模板中使用时,ref 会自动解包
//【在模板渲染上下文中,只有顶级的 ref 属性才会被解包。】
<button @click="count++">
{{ count }}
</button>
//不能解包的非顶级ref属性
const count = ref(0)
const object = { id: ref(1) }
{{ object.id }} //不需要计算时 可以解包
{{ object.id + 1 }}//object.id不能自动解包 结果将是 [object Object]1
//解决这个问题:将 id 解构为一个顶级属性
const { id } = object
{{ id + 1 }}//此时模版可以正常解包
/*更复杂的逻辑,在同一作用域内声明更改函数,在其中更改ref
并将它们作为方法与状态一起公开*/
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// 在 JavaScript 中需要 .value
count.value++
}
// 不要忘记同时暴露 increment 函数
return {
count,
increment//暴露的方法可以被用作事件监听器
}
}
}
<button @click="increment">
{{ count }}
</button>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 在 setup() 函数中手动暴露大量的状态和方法非常繁琐。可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用
来大幅度地简化代码
大部分开发者都会使用这个
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 为什么设计ref时候要带.value?这个涉及到Vue响应式系统工作原理。
当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。
- 在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。
该 .value 属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。 - 与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到。
可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。
// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
//深度响应性
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// 以下都会按照期望工作
obj.value.nested.count++
obj.value.arr.push('baz')
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。
当 ref 作为reactive()响应式数组或原生集合类型 (如 Map) 中的元素被访问时,它不会被解包。
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- DOM 更新时机
当你修改了响应式状态时,DOM 会被自动更新。DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API,如果在DOM没更新完之前执行,拿到的可能还是旧的内容。
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()//这个 nextTick()作用是等待DOM更新完成之后再执行后续
// 现在 DOM 已经更新了
//后续操作
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- reactive()
另一种声明响应式状态的方式,与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性,通过 reactive() 转换为响应式代理。与浅层 ref 类似,这里也有一个 shallowReactive() API 可以选择退出深层响应性。
响应式对象是 JavaScript 代理Proxy,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。
不要再reactive()里包ref()
reactive() 创建的对象 不会被自动解包。- ref和reactive都会将对象包装
reactive() 将深层地转换对象:当访问嵌套对象时,它们也会被 reactive() 包装。当 ref 的值是一个对象时,ref() 也会在内部调用它。
- ref和reactive都会将对象包装
import { reactive } from 'vue'
const state = reactive({ count: 0 })
//模版中
<button @click="state.count++">
{{ state.count }}
</button>
- 1
- 2
- 3
- 4
- 5
- 6
* reactive() API局限性
- 1
由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。
有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。
不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失
let state = reactive({ count: 0 })
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })
- 1
- 2
- 3
- 4
对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接。这个地方,ref也同理,用新的ref替换旧的ref或其属性,都会与原来响应式对象失去联系。
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1
----------------------------------
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
count++// 不会影响原始的 state
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
callSomeFunction(state.count)//传的是一个基本类型的值(数值),而不是引用对象。
/*
所以:state.count 的值会被“解包”为一个普通的数字(比如 0、1、2)
这个数字是非响应式的
在函数 callSomeFunction 里,如果你只是拿这个值用,是没问题的
但不能让 callSomeFunction 能感知到响应式变化(比如依赖追踪、watch 触发)
*/
// 我们必须传入整个对象以保持响应性
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 对比reactive Proxy vs. Original原生
原始对象 不等于 原始对象的 Proxy: reactive() 返回的是一个原始对象的 Proxy,和原始对象是不相等。只有代理对象才是响应式的,原始对象的更改不会触发更新。所以,使用 Vue 的响应式系统是仅使用你声明对象的代理版本。
为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身。
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理。
const raw = {}
const proxy = reactive(raw)
// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false
// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true
// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
/*
虽然你把 raw 这个普通对象赋值给了 proxy.nested,
但在访问 proxy.nested 的时候,Vue 自动把 raw 转换成响应式对象了,
所以你拿到的是它的响应式代理版本,不是原始对象 raw 本身。
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
计算属性
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。使用计算属性来描述依赖响应式状态的复杂逻辑。
- 高亮部分为计算属性的特性
下面这个例子中定义一个计算属性 publishedBooksMessage。
computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value。
Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage 依赖于 author.books,所以当 author.books 改变时,任何依赖于 publishedBooksMessage 的绑定都会同时更新。计算结果会被缓存,并只有在其依赖发生改变时才会被自动更新。
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'//自动包装,返回计算属性ref
})
script>
<template>
<p>Has published books:p>
<span>{{ publishedBooksMessage }}span>
template>
<script setup>
import { ref, computed } from 'vue'
let id = 0
const newTodo = ref('')
const hideCompleted = ref(false)
const todos = ref([
{ id: id++, text: 'Learn HTML', done: true },
{ id: id++, text: 'Learn JavaScript', done: true },
{ id: id++, text: 'Learn Vue', done: false }
])
const filteredTodos = computed(() => {
return hideCompleted.value
? todos.value.filter((t) => !t.done)
: todos.value
})
function addTodo() {
todos.value.push({ id: id++, text: newTodo.value, done: false })
newTodo.value = ''
}
function removeTodo(todo) {
todos.value = todos.value.filter((t) => t !== todo)
}
script>
<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todobutton>
form>
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input type="checkbox" v-model="todo.done">
<span :class="{ done: todo.done }">{{ todo.text }}span>
<button @click="removeTodo(todo)">Xbutton>
li>
ul>
<button @click="hideCompleted = !hideCompleted">
{{ hideCompleted ? 'Show all' : 'Hide completed' }}
button>
template>
<style>
.done {
text-decoration: line-through;
}
style>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 计算属性缓存 vs 方法
这个同样能实现跟前面例子相同的结果。{{ calculateBooksMessage() }}
- 若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的
- 计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。【优点:提高性能】
这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。
比如const now = computed(() => Date.now())
其永远不会更新,因为 Date.now() 并不是一个响应式依赖。相比之下,方法调用总是会在重渲染发生时再次执行函数。
- 只读?可写?
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建。- 注意:
Getter 不应有副作用,计算属性的 getter 应只做计算而没有任何其他的副作用,不要改变其他状态、在 getter 中做异步请求或者更改 DOM。
避免直接修改计算属性值 :从计算属性返回的值是派生状态。把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——更新它所依赖的源状态以触发新的计算。
- 注意:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
fullName.value = 'John Doe' //setter被调用,并更新计算属性
//尽量避免修改fullName而是修改firstName,lastName
script>
<script setup>
import { ref, computed } from 'vue'
const count = ref(2)
// 这个计算属性在 count 的值小于或等于 3 时,将返回 count 的值。
// 当 count 的值大于等于 4 时,将会返回满足我们条件的最后一个值
// 直到 count 的值再次小于或等于 3 为止。
const alwaysSmall = computed((previous) => {//previous记录上一个值
if (count.value <= 3) {
return count.value
}
return previous
})
script>
<script setup>
import { ref, computed } from 'vue'
const count = ref(2)
const alwaysSmall = computed({
get(previous) {
if (count.value <= 3) {
return count.value
}
return previous
},
set(newValue) {
count.value = newValue * 2
}
})
script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
类与样式绑定
class 和 style 都是 attribute,可以和其他 attribute 一样使用 v-bind 将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。
Vue 专门为 class 和 style 的 v-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
可以在对象中写多个字段来操作多个 class。此外,:class 指令也可以和一般的 class attribute 共存。
- 绑定内联样式
:style 支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style 属性
:stype里的属性名支持骆驼命名法、kebab-cased烤肉串命名法(即单词之间用-
连接)
<script setup>
const activeColor = ref('red')
const fontSize = ref(30)
const styleObject = reactive({
color: 'red',
fontSize: '30px'
})
script>
<template>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }">div>
<div :style="{ 'font-size': fontSize + 'px' }">div>
<div :style="styleObject">div>
<div :style="[baseStyles, overridingStyles]">div>
template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 绑定对象
<script setup>
const isActive = ref(true)
const hasError = ref(false)
const classObject = reactive({//直接绑定一个对象
active: true,
'text-danger': false
})
script>
<template>
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
>div>
<div :class="classObject">div>
template>
<script setup>
const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': error.value && error.value.type === 'fatal'//致命错误
}))
script>
<template>
<div :class="classObject">div>
template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 绑定数组
<script setup>
const activeClass = ref('active')
const errorClass = ref('text-danger')
script>
<template>
<div :class="[activeClass, errorClass]">div>
<div :class="[isActive ? activeClass : '', errorClass]">div>
<div :class="[{ [activeClass]: isActive }, errorClass]">div>
template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 在组件上使用
<p class="foo bar">Hi!p>
<MyComponent class="baz boo" />
//此时子组件+父组件给到的class会叠加 得到class="foo bar baz boo"
//可以结合之前的JS表达式 同理
<MyComponent :class="{ active: isActive }" />
<template>
<button v-bind="$attrs">点击button>
template>
<script setup>
defineProps(['type'])
/* 假设只接收 type,type="submit" 会被当作 props
class="btn" 和 id="my-btn" 不会被接收,Vue 会把它们放进 $attrs
所以$attrs是
{
class: 'btn',
id: 'my-btn'
}
*/
script>
<MyButton type="submit" class="btn" id="my-btn" />
<p :class="$attrs.class">Hi!p>
<span>This is a child componentspan>
<MyComponent class="baz" />
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 浏览器自动前缀
当你在 :style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。 - 样式多值:可以对一个样式属性提供多个 (不同前缀的) 值
数组仅会渲染浏览器支持的最后一个值。
条件渲染
若为假值,元素将被从 DOM 中移除。
v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。
当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 会首先被执行。同时使用 v-if 和 v-for 是不推荐的,因为这样二者的优先级不明显。
- v-if 指令来有条件地渲染元素,v-else 和 v-else-if
- 原理:直接从DOM树上摘下挂上;v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
- 原理:直接从DOM树上摘下挂上;v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
< template> 上也可以挂这些 v-if
- v-show
- 原理:切换display:none;
v-show 不支持在 < template> 元素上使用,也不能和 v-else 搭配使用。
列表渲染
v-for 指令来渲染一个基于源数组的列表。
<li v-for="{ message } in items">
{{ message }}
li>
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
li>
<li v-for="item in items">
<span v-for="childItem in item.children">
{{ item.message }} {{ childItem }}
span>
li>
<div v-for="item of items">div>
<script setup>
const myObject = reactive({
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
})
script>
<template>
<ul>
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
li>
ul>
template>
<span v-for="n in 10">{{ n }}span>
<ul>
<template v-for="item in items">
<li>{{ item.msg }}li>
<li class="divider" role="presentation">li>
template>
ul>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- v-for 与 v-if
当它们同时存在于一个节点上时,v-if 比 v-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名
在外先包装一层 < template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读)
同时使用 v-if 和 v-for 是不推荐的,因为这样二者的优先级不明显。
两种常见的情况可能导致这种用法:
过滤列表中的项目 (例如,v-for=“user in users” v-if=“user.isActive”)。在这种情况下,可以用一个新的计算属性来替换 users,该属性返回过滤后的列表 (例如 activeUsers)。
==避免渲染应该隐藏的列表 ==
(例如 v-for=“user in users” v-if=“shouldShowUsers”)。在这种情况下,将 v-if 移至容器元素 (如 ul、ol)。
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
li>
template>
- 1
- 2
- 3
- 4
- 5
- 通过 key 管理状态
Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
默认模式是高效的,但只适用于渲染结果 不依赖 子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。如果你没有加 :key,Vue 默认是“就地复用(in-place patching)”:Vue 会尝试复用已有的 DOM 元素或组件实例,而不是销毁重建;这样做性能更高,因为减少了 DOM 操作和组件销毁重建的开销。
如果你渲染的每一项有自己的状态,比如:
子组件有自己的内部数据(比如输入框的值)
子组件在 mounted 做了副作用处理
每一项需要维持自己的状态(动画、焦点、上传中状态)
Vue 复用组件时不会自动重置这些状态!
举个例子🌰,没加 key时如果你操作 list,比如把第一项删了,Vue 会复用第二个 < MyInput> 的组件实例来展示新的第一项。❗ 结果就是:用户在输入框里打的内容会跳到别的项里,或状态错乱。
加了唯一的:key,Vue 会根据 key 判断每一项是不是“新项”,只复用 key 相同的组件,否则就销毁重建,状态自然不会错乱。为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute。不要用对象作为 v-for 的 key。 - 注意,我们还给每个 todo 对象设置了唯一的 id,并且将它作为特殊的 key attribute 绑定到每个 < li >。key 使得 Vue 能够精确地移动每个 < li >,以匹配对应的对象在数组中的位置。
<script setup>
defineProps(['title'])
defineEmits(['remove'])
script>
<template>
<li>
{{ title }}
<button @click="$emit('remove')">Removebutton>
li>
template>
<script setup>
import { ref } from 'vue'
import TodoItem from './TodoItem.vue'
const newTodoText = ref('')
const todos = ref([
{
id: 1,
title: 'Do the dishes'
},
{
id: 2,
title: 'Take out the trash'
},
{
id: 3,
title: 'Mow the lawn'
}
])
let nextTodoId = 4
function addNewTodo() {
todos.value.push({
id: nextTodoId++,
title: newTodoText.value
})
newTodoText.value = ''
}
script>
<template>
<form @submit.prevent="addNewTodo">
<label for="new-todo">Add a todolabel>
<input
v-model="newTodoText"
id="new-todo"
placeholder="E.g. Feed the cat"
/>
<button>Addbutton>
form>
<ul>
<todo-item
v-for="(todo, index) in todos"
:key="todo.id"
:title="todo.title"
@remove="todos.splice(index, 1)"
>todo-item>
ul>
template>
<script setup>
import { ref } from 'vue'
// 给每个 todo 对象一个唯一的 id
let id = 0
const newTodo = ref('')
const todos = ref([
{ id: id++, text: 'Learn HTML' },
{ id: id++, text: 'Learn JavaScript' },
{ id: id++, text: 'Learn Vue' }
])
function addTodo() {
todos.value.push({ id: id++, text: newTodo.value })
newTodo.value = ''
}
function removeTodo(todo) {
todos.value = todos.value.filter((t) => t !== todo)
}
script>
<template>
<form @submit.prevent="addTodo">
<input v-model="newTodo" required placeholder="new todo">
<button>Add Todobutton>
form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo)">Xbutton>
li>
ul>
template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 数组变化侦测
- Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse() - 替换一个数组
变更方法就是会对调用它们的原数组进行变更。相对地,也有一些不可变 (immutable) 方法,例如 filter(),concat() 和 slice(),这些都不会更改原数组,而总是返回一个新数组。当遇到的是非变更方法时,我们需要将旧的数组替换为新的:Vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作,不会导致 Vue 丢弃现有的 DOM 并重新渲染整个列表
// `items是一个数组的 ref
items.value = items.value.filter((item) => item.message.match(/Foo/))
- 1
- 2
3)展示过滤或排序后的结果
显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据。在这种情况下,你可以创建返回已过滤或已排序数组的计算属性。
在计算属性不可行的情况下 ,用函数返回这个结果。
const numbers = ref([1, 2, 3, 4, 5])
const evenNumbers = computed(() => {
return numbers.value.filter((n) => n % 2 === 0)
})
<li v-for="n in evenNumbers">{{ n }}</li>
const sets = ref([
[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10]
])
function even(numbers) {
return numbers.filter((number) => number % 2 === 0)
}
<ul v-for="numbers in sets">
<li v-for="n in even(numbers)">{{ n }}</li>
</ul>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
在计算属性中使用 reverse() 和 sort() 的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本:
- return numbers.reverse()
+ return [...numbers].reverse()
- 1
- 2
事件处理
- 事件监听
- 访问原生 DOM 事件:向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数
- 事件修饰符
.stop 阻止事件冒泡
.prevent 阻止默认行为
.self 仅当 event.target 是元素本身时才会触发事件处理器
.capture捕获模式,指向内部元素的事件,在被内部元素处理前,先被外部处理
.once程序运行期间, 只触发一次事件处理函数
.passive一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能。
使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用 @click.prevent.self 会阻止元素及其子元素的所有点击事件的默认行为,而 @click.self.prevent 则只会阻止对元素本身的点击事件的默认行为。
.capture、.once 和 .passive 修饰符与原生 addEventListener 事件相对应
请勿同时使用 .passive 和 .prevent,它们是相反的两种,因为 .passive 已经向浏览器表明了你不想阻止事件的默认行为。如果你这么做了,则 .prevent 会被忽略,并且浏览器会抛出警告。
<a @click.stop="doThis">a>
<form @submit.prevent="onSubmit">form>
<a @click.stop.prevent="doThat">a>
<form @submit.prevent>form>
<div @click.self="doThat">...div>
<div @click.capture="doThis">...div>
<a @click.once="doThis">a>
<div @scroll.passive="onScroll">...div>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
<script setup>
const count = ref(0)
function warn(message, event) {
// 这里可以访问原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
script>
<template>
<button @click="count++">Add 1button>
<p>Count is: {{ count }}p>
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
button>
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
button>
template>
<script setup>
const name = ref('Vue.js')
function greet(event) {
alert(`Hello ${name.value}!`)
// `event` 是 DOM 原生事件
if (event) {//event 在template中的使用方式下永远是一个合法的 MouseEvent
alert(event.target.tagName)
}
}
function say(message) {
alert(message)
}
script>
<template>
<button @click="greet">Greetbutton>
<button @click="say('hello')">Say hellobutton>
<button @click="say('bye')">Say byebutton>
template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 按键事件、修饰符
Vue 为一些常用的按键提供了别名:
.enter
.tab
.delete (捕获“Delete”和“Backspace”两个按键)
.esc
.space
.up
.down
.left
.right
<script setup>
script>
<template>
<input @keyup.enter="submit" />
<input @keyup.page-down="onPageDown" />
<input @keyup.alt.enter="clear" />
<div @click.ctrl="doSomething">Do somethingdiv>
<button @click.ctrl="onClick">Abutton>
<button @click.ctrl.exact="onCtrlClick">Abutton>
<button @click.exact="onClick">Abutton>
template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 鼠标按键修饰符
这些修饰符将处理程序限定为由特定鼠标按键触发的事件。
.left
.right
.middle
但请注意,.left,.right 和 .middle 这些修饰符名称是基于常见的右手用鼠标布局设定的,但实际上它们分别指代设备事件触发器的“主”、”次“,“辅助”,而非实际的物理按键。因此,对于左手用鼠标布局而言,“主”按键在物理上可能是右边的按键,但却会触发 .left 修饰符对应的处理程序。又或者,触控板可能通过单指点击触发 .left 处理程序,通过双指点击触发 .right 处理程序,通过三指点击触发 .middle 处理程序。同样,产生“鼠标”事件的其他设备和事件源,也可能具有与“左”,“右”完全无关的触发模式。
表单绑定
同时使用 v-bind 和 v-on 来在表单的输入元素上创建双向绑定(UI和数据);
为了简化双向绑定,Vue 提供了一个 v-model 指令,它实际上是上述操作的语法糖,v-model 会将被绑定的值与 < input> 的值自动同步,这样我们就不必再使用事件处理函数了。v-model 不仅支持文本输入框,也支持诸如多选框、单选框、下拉框之类的输入类型。
文本类型的 < input> 和 < textarea> 元素会绑定 value property 并侦听 input 事件;
< input type=“checkbox”> 和 < input type=“radio”> 会绑定 checked property 并侦听 change 事件;
< select> 会绑定 value property 并侦听 change 事件。
对于需要使用 IME 的语言 (中文,日文和韩文等),v-model 不会在 IME 输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的 input 事件监听器和 value 绑定而不要使用 v-model。
//v-bind v-on双向绑定
<script setup>
import { ref } from 'vue'
const text = ref('')
function onInput(e) {//输入时就把text值改变
text.value = e.target.value
}
const checkedNames = ref([])
//checkedNames 数组将始终包含所有当前被选中的框的值。
const selected = ref('A')
const options = ref([
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
])
</script>
<template>
<input :value="text" @input="onInput" placeholder="Type here">
<p>{{ text }}</p>//text值改变会将p里的值改变
//如果不需要p的话 不需要绑定text 因为input本身输入的值自然会显示在上面
<!--
简化 使用`v-model`
-->
<input v-model="text">
<!-- 错误:textarea不支持插值表达式 -->
<textarea>{{ text }}</textarea>
<!-- 正确 -->
<textarea v-model="text"></textarea>
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>
<!--将多个复选框绑定到同一个数组或集合的值-->
<div>Checked names: {{ checkedNames }}</div>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>
<div>Picked: {{ picked }}</div>
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>
<div>Selected: {{ selected }}</div>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<!--
如果 v-model 表达式的初始值不匹配任何一个选择项,
<select> 元素会渲染成一个“未选择”的状态。
在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。
-->
<div>Selected: {{ selected }}</div>
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<select v-model="selected">
<option v-for="option in options" :value="option.value">
{{ option.text }}
</option>
</select>
<div>Selected: {{ selected }}</div>
</template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
select单选:要有一个禁用的初始项
侦听器watch
有时需要响应性地执行一些“副作用”——例如,当一个数字改变时将其输出到控制台。通过侦听器来实现它。
watch() 可以直接侦听一个 ref,并且只要值改变就会触发回调。watch() 也可以侦听其他类型的数据源。
<script setup>
import { ref, watch } from 'vue'
const todoId = ref(1)
const todoData = ref(null)
async function fetchData() {
todoData.value = null
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
todoData.value = await res.json()
}
fetchData()
watch(todoId, fetchData)
</script>
<template>
<p>Todo id: {{ todoId }}</p>
<button @click="todoId++" :disabled="!todoData">Fetch next todo</button>
<p v-if="!todoData">Loading...</p>
<pre v-else>{{ todoData }}</pre>
</template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
生命周期
模版引用
有时不可避免地需要手动操作 DOM。这时需要使用模板引用——也就是指向模板中一个 DOM 元素的 ref。我们需要通过这个特殊的 ref attribute 来实现模板引用,并且在组件挂载之后访问它。要在这个时期则需要用到生命周期钩子——它允许注册一个在组件的特定生命周期调用的回调函数。详见生命周期章节
<p ref="pElementRef">hello</p>
//要访问该引用,我们需要声明一个同名的 ref:
const pElementRef = ref(null)
/*
注意这个 ref 使用 null 值来初始化。
这是因为当