关于mvvm:MVI到底是不是凑数的通过案例与MVVM进行比较

前言最近看到不少介绍MVI架构,即Model-View-Intent的文章,有人留言说Google炒冷饭或者为了凑KPI“创造”了MVI这么一个词。和后端的敌人形容了一下,他们听了第一印象也是和MVVM如同区别不大。然而凭印象Google应该还没有到须要这样来凑数。 去看了一下官网,发现齐全没有提到MVI这个词。。然而举荐的架构图的确是更新了,用来演示MVI也的确很搭。 (官网图) 想了想,决定总结一下本人的发现,和掘友们一起探讨学习。 案例分享看过一些剖析MVI的文章,外面实现的办法各种各样,细节也不尽相同。甚至对于Model边界的划分也会不一样。 上面先分享一下在特定场景下我的MVVM和MVI实现(不重要的细节会省略)。 场景先预设一个场景,咱们的界面(View/Fragment)里有一个锅。次要工作就是实现一道菜的烹饪: flowchart LR停火 --> 热油 --> 加菜 --> 加调料 --> 出锅 几个须要留神的点: 初始状态:停火退出资料时:都是异步获取资料,再退出锅中完结状态:出锅本文次要是比拟MVVM和MVI,这里只分享这两种实现。 经典MVVM为了增强比照,这里的实现比拟靠近Android Architecture Components刚公布时官网的的代码架构和片段: (过后的官网图) // PotFragment.ktclass PotFragment { ... // 察看是否点火 viewModel.fireStatus.observe( viewLifecycleOwner, Observer { updateUi() if (fireOn) addOil() } ) // 察看油温 viewModel.oilTemp.observe( viewLifecycleOwner, Observer { updateUi() if (oilHot) addIngredients() } ) // 察看菜熟没熟 viewModel.ingredientsStatus.observe( viewLifecycleOwner, Observer { updateUi() if (ingredientsCooked) { // 加调料 addPowder(SALT) addPowder(SOY_SAUCE) } } ) // 察看油盐是否加完 viewModel.allPowderAdded.observe( viewLifecycleOwner, Observer { // 出锅! } ) viewModel.loading.observe( viewLifecycleOwner, Observer { if (loading) { // 颠勺 } else { // 放下锅 } } ) // 所有准备就绪,点火 turnOnFire() ...}// PotViewModel.ktclass PotViewModel(val repo: CookingRepository) { private val _fireStatus = MutableLiveData<FireStatus>() val fireStatus: LiveData<FireStatus> = _fireStatus private val _oilTemp = MutableLiveData<OilTemp>() val oilTemp: LiveData<OilTemp> = _oilTemp private val _ingredientsStatus = MutableLiveData<IngredientsStatus>() val ingredientsStatus: LiveData<IngredientsStatus> = _ingredientsStatus // 所有调料加好了才更新。这里Event外部会有flag提醒这个LiveData的更新是否被应用过 //(当年咱们还真用这种形式实现过单次生产的LiveData)。 private val _allPowderAdded = MutableLiveData<Event<Boolean>>() val allPowderAdded: LiveData<Event<Boolean>> = _allPowderAdded // 假如曾经实现逻辑从repo获取是否有还在进行的数据获取 private val _loading = MutableLiveData<Boolean>() val loading: LiveData<Boolean> = _loading fun turnOfFire() {} // 假如上面都是异步获取资料,这里简化一下代码 fun addOil() { repo.fetchOil() } fun addIngredients() { repo.fetchIngredients() } fun addPowder(val powderType: PowderType) { repo.fetchPowder(powderType) // 更新_allPowderAdded的逻辑会在这里 } ...}特点: ...

April 25, 2022 · 3 min · jiezi

关于mvvm:彻底理解Android架构重构Jetpack-MVVM

前言汇聚了业界出名架构文章。从建筑学的常识中失去一些对架构的思考,并以架构设计准则和目标对Jetpack MVVM 从新结构!github 后续本我的项目将继续更新,并欠缺 wanAndorid 的所有性能。还会用 23 种设计模式在我的项目中实际,彻底了解设计模式在业务场景中的应用,欢送关注 Github:https://github.com/blindmonk/WanArchitecture 一、什么是架构1.1 架构介绍架构到底是什么?如何更好的了解架构。咱们晓得中国文字博大精深能够说从文字的组成就能了解其含意。架构也不例外 “架构” 是由 “架” 、“构” 组成。 架:建造、搭设、撑持。简称:整体构造 构:屋宇、供人寓居的木、砖瓦构筑物。简称:组件整体构造和组件的组合就造成了架构。以 Android 架构为例子一个 APP 通常是有 class(类)组成,而这些 class 之间如何如何组合、相互之间如何产生作用,则是影响这个 APP 自身的关键点。细分的话能够分为类、接口(连接器)、工作流。所谓类就是组成架构的外围 “砖瓦”,而接口则是这些类之间通信的门路、通信的机制、通信的冀望后果。工作流则是形容零碎如何应用类和接口实现某一项需要比方:一次网络申请。下面介绍架构中提到了屋宇、木头、砖瓦可见架构和修建有着彼此的分割。 1.2 建筑学上世纪 60 年代曾经设计软件架构这个概念了,到了 90 年代软件架构这个概念才开始流行起来。而计算机的历史开始于上世纪五十年代相比修建历史就十分短暂了,建筑工程从石器时代就开始了。人类在几千年的建筑设计实际中积攒了大量的教训和教训,建筑设计基本上蕴含两点,一是建筑风格,二是修建模式。独特的建筑风格和失当抉择的修建模式,能够使它成为一个举世无双的修建。 下图的照片显示了现代玛雅修建:Chichen-Itza,九个微小的石级堆垒而上,九十一级台阶(象征着四季的天数)夺路而出,塔顶的神殿耸入云天。所有的数字都如日历般谨严,格调雄浑。难以想象这是石器时代的建筑物。 英国首相丘吉尔说,咱们结构建筑物,修建也结构咱们,英国下议院的会议厅较狭隘,无奈使所有的下议院议员面向同一个方向入座,而必须分成两侧入座。丘吉尔认为,议员们入座的时候天然会抉择与本人政见雷同的人同时入座,而这就是英国政党制的起源。 二、架构设计目标 简直所有的软件设计理念都能够在浩瀚的建筑学历史中找到。许多人认为 “模式必须遵从性能”(你认同这种观点吗?欢送在评论区留下你的认识)。而好的设计既有模式又有性能。比方咱们的北京大兴国内机场大兴机场以航站楼为外围向周围延展从地面鸟瞰就像是一只展翅欲飞的凤凰,以航站楼核心区为核心,别离向西南、西北、中南、东北、东南五个方向伸出了五条指廊,通往北京大兴国内机场的飞行区。这种从核心向五湖四海延长的设计,使航站楼中心点到最远端登机口的间隔只有 600 米左右,旅客步行返回最多只需 8 分钟。 修建的设计又有肯定的目的性,而软件架构设计也同理。软件架构目的性大抵可分为可扩展性、可定制化、可伸缩、可维护性: 1. 可扩展性: APP 必须可能在用户的 UV/PV 数量疾速减少的状况下,放弃软件正当的性能。只有这样在疾速的从 0 到 1 的需要迭代中能力后顾无忧。 2. 可定制化: 在同一个软件系统中可能面向的用户群体是不同的、多样的,须要满足依据用户群的不同和市场需求的不同进行定制化。比方一个 APP 中某些性能只针对特定用户凋谢。 3. 可伸缩性: 在新技术呈现的时候,一个软件系统该当容许接入新技术,从而对现有零碎进行性能和性能的扩大。 4. 可维护性: 软件系统的保护包含两方面,一是修复现有的 bug,二是将新的迭代需要开发到现有零碎中去。一个易于保护的零碎能够无效地升高人力和物力。 三、实际一个 APP:玩 Android 针对上面对架构的介绍,置信曾经从生疏走向相熟了。然而最重要的还是实际,平凡的毛主席已经说过 你要想晓得梨子的味道,就要亲口尝一下。因而借用了 wanAndoird 凋谢 API 简略实现一个 APP 并概括上述架构的关键点,次要的性能点如下: ...

March 4, 2022 · 2 min · jiezi

关于mvvm:使用-Source-Generators-快速编写-MVVM-代码

大家好,我是本期的微软 MVP 实验室研究员——陈锦华。微软 MVP ( Windows Development 方向),专一于 .NET 开发,有十多年的客户端开发教训。当初热衷于撰写博客,分享 WPF、UWP 和 Azure DevOps 相干的教训。 0.1. 对于 MVVM Toolkit.NET Community Toolkit 是以用于所有 .NET 开发人员的帮忙类和 API 的合集,并且与任何特定 UI 平台无关。 最它公布了 8.0.0 preview1 版本,它蕴含了从 Windows Community Toolkit 迁徙过去的以下组件: • CommunityToolkit.Common • CommunityToolkit.Mvvm • CommunityToolkit.Diagnostics • CommunityToolkit.HighPerformance 其中 CommunityToolkit.Mvvm 又名 MVVM Toolkit ,它是一个现代化、疾速以及模块化的 MVVM 库。它蕴含一个 Source Generators 组件:MVVM Toolkit source generators。这篇文章将介绍它如何帮忙开发着疾速编写 MVVM 代码。 0.2. MVVM Toolkit source generatorsSource Generators 是一项 C# 编译器性能,使 C# 开发人员可能在编译用户代码时进行查看,并动静生成新的 C# 源文件,以增加到用户的编译中。 通过这种形式,你的代码能够在编译过程中运行并查看你的程序以生成与其余代码一起编译的其余源文件。 对 MVVM 平台的开发者来说,Source Generators 是一个期待已久的新性能,毕竟 MVVM 模式中,开发者须要写很多命令和属性的额定的模板代码。到目前为止,为了加重这些额定代码的累赘,微软和其它开发者想出过很多不同的计划,而 Source Generators 看起来会是更好的一个。MVVM Toolkit 已开始了这方面的工作,8.0 版本又失去进一步的增强,当初 MVVM Toolkit source generators 是一个增量生成器,性能将会晋升很多。 上面将简略解说如何应用 MVVM Toolkit source generators 命令和属性的代码。 ...

February 16, 2022 · 2 min · jiezi

关于mvvm:WPF-DataTemplate与ControlTemplate结合使用

如深入浅出WPF中的形容,DataTemplate为数据的外衣,ControlTemplate为控件的外衣。ControlTemplate管制控件的款式,DataTemplate控制数据显示的款式,DataTemplate是ControlTemplate的一部分。本文介绍DataTemplate与ControlTemplate联合应用的办法,其关键在于ContentPresenter,它是DataTemplate的树根,代表DataTemplate的实例。 场景自定义Button,使其显示当前页与总页数,当页码变动时自动更新。 实现步骤自定义Button.ControlTemplate;自定义Button.ContentTemplate;创立数据类;创立ViewModel类;绑定。示例代码:// xaml<UserControl.Resources> <viewmodel:TextViewModel x:Key="TestViewModel"/></UserControl.Resources><Grid DataContext="{StaticResource TextViewModel}"> <Button Width="120" Height="50" Content="{Binding PageInfo}"> <Button.Template> <ControlTemplate TargetType="Button"> <ContentPresenter/> </ControlTemplate> </Button.Template> <Button.ContentTemplate> <DataTemplate> <TextBlock Width="{TemplateBinding Width}" TextAlignment="Center" FontSize="36" FontFamily="微软雅黑" Foreground="#ffffff"> <Run Text="{Binding CurrentPage}"/> <Run Text="/"/> <Run Text="{Binding TotalPages}"/> </TextBlock> </DataTemplate> </Button.ContentTemplate> </Button></Grid>// 数据类public class PageInfo : ViewModelBase{ public PageInfo(string currentPage, string totalPages) { this.CurrentPage = currentPage; this.TotalPages = totalPages; } public string CurrentPage { get { return currentPage; } set { currentPage = value; OnPropertyChanged("CurrentPage"); } } public string TotalPages { get { return totalPages; } set { totalPages = value; OnPropertyChanged("TotalPages"); } } private string currentPage; private string totalPages;}// viewmodel类public class TestViewModel : ViewModelBase{ public TextViewModel() { PageInfo = new PageInfo("1", "1"); } public PageInfo PageInfo { get { return pageInfo; } set { pageInfo = value; } } // 其它逻辑 private PageInfo pageInfo;}

February 24, 2021 · 1 min · jiezi

稍微学一下-MVVM-原理

博客原文 介绍本文通过仿照 Vue ,简单实现一个的 MVVM,希望对大家学习和理解 Vue 的原理有所帮助。 前置知识nodeTypenodeType 为 HTML 原生节点的一个属性,用于表示节点的类型。 Vue 中通过每个节点的 nodeType 属性是1还是3判断是元素节点还是文本节点,针对不同类型节点做不同的处理。 DocumentFragmentDocumentFragment是一个可以被 js 操作但不会直接出发渲染的文档对象,Vue 中编译模板时是现将所有节点存到 DocumentFragment 中,操作完后再统一插入到 html 中,这样就避免了多次修改 Dom 出发渲染导致的性能问题。 Object.definePropertyObject.defineProperty接收三个参数 Object.defineProperty(obj, prop, descriptor), 可以为一个对象的属性 obj.prop t通过 descriptor 定义 get 和 set 方法进行拦截,定义之后该属性的取值和修改时会自动触发其 get 和 set 方法。 从零实现一个类 Vue以下代码的 git 地址:以下代码的 git 地址目录结构├── vue│ ├── index.js│ ├── obsever.js│ ├── compile.js│ └── watcher.js└── index.html实现的这个 类 Vue 包含了4个主要模块: index.js 为入口文件,提供了一个 Vue 类,并在类的初始化时调用 obsever 与 compile 分别进行数据拦截与模板编译;obsever.js 中提供了一个 Obsever 类及一个 Dep 类,Obsever 对 vue 的 data 属性遍历,给所有数据都添加 getter 与 setter 进行拦截,Dep 用于记录每个数据的依赖;compile.js 中提供了一个 Compile 类,对传入的 html 节点的所有子节点遍历编译,分析 vue 不同的指令并解析 {{}} 的语法;watcher.js 中提供了一个 Watcher 类,用于监听每个数据的变化,当数据变化时调用传入的回调函数;入口文件在 index.html 中是通过 new Vue() 来使用的: ...

October 8, 2019 · 4 min · jiezi

基于Vue的MVVM学习笔记

什么是MVVMMVVM——Model-View-ViewModle的缩写,MVC设计模式的改进版。Model是我们应用中的数据模型,View是我们的UI层,通过ViewModle,可以把我们Modle中的数据映射到View视图上,同时,在View层修改了一些数据,也会反应更新我们的Modle。 上面的话,未免太官方了。简单理解就是双向数据绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。 MVVM这种思想的前端框架其实老早就有了,我记得是在13年,自己在公司的主要工作是做后台管理系统的UI设计和开发,当时就思考,如何让那些专注后台的开发,既简单又方便的使用前端开发的一些组件。当时有三种方案: 使用Easy-ui,但easy-ui好像官方要求收费,当然也可以破解使用自己开发UI框架,其实当时想做的东西就是后来BootStrap使用谷歌的Angular,进行二次开发后来的评估是: 使用easy-ui,工作量太多使用Angular和easy-ui不仅工作量很大,后台也要做相应的修改自己写UI框架,比较合适,当时的做法是写一些jQuery相关的插件,先给后台一个js插件包,后续的UI修改,慢慢进行。当时自己还是比较推崇Angular的,我记得后来还买了一本《基于MVC的Javascript Web富应用开发》专门去了解这种模式在工作中可能用的情况,以及实现它的一些基本思路。 当时热点比较高的MVVM框架有: Angular:谷歌出品,名气很大,入门高,使用麻烦,它提供了很多新的概念。Backbone.js,入门要求级别很高,我记得当时淘宝有些项目应用了这个,《基于MVC富应用开发》书里面也是以这个框架为主介绍MVC的。Ember:大而全的框架,开始写代码之前就已经有很多的工作要做了。当年的环境和条件都没有现在好,无论从技术完善的情况,还是工作的实际情况上面看,都是如此——那时候前后端分离都是理想。 当然现在环境好了,各种框架的出现也极大方便了我们,提高了我们开发的工作效率。时代总是在进步,大浪淘沙,MVVM的框架现在比较热门和流行的,我相信大家现在都知道,就是下面三种了: AngularVueReact现在Angular除了一些忠实的拥趸,基本上也就没落了。Angular无论从入门还是实际应用方面,都要比其他两个框架发费的时间成本更大。Angular现在有种英雄末路的感觉,但不能不承认,之前它确实散发了光芒。 Angular的1.x版本,是通过脏值检测来实现双向绑定的。 而最新的Angular版本和Vue,以及React都是通过数据劫持+发布订阅模式来实现的。 脏值检测 简单理解就是,把老数据和新数据进行比较,脏就表示之前存在过,有过痕迹,通过比较新旧数据,来判断是否要更新。感兴趣的可以看看这篇文章 构建自己的AngularJS,第一部分:作用域和digest。 数据劫持 发布订阅 数据劫持:在访问或者修改对象的某个属性时,通过代码拦截这个行为,进行额外的操作或者修改返回结果。在ES5当中新增了Object.defineProperty()可以帮我们实现这个功能。 发布订阅:现在每个人应该都用微信吧,一个人可以关注多个公众号,多个人可以同时关注相同的公众号。关注的动作就相当于订阅。公众号每周都会更新内容,并推送给我们,把写好的文章在微信管理平台更新就好了,点击推送,就相当于发布。更详细的可以深入阅读 javascript设计模式——发布订阅模式 怎么实现一个MVVM我们静下心好好思考下,如果才能实现双向数据绑定的功能。可能需要: 一个初始化实例的类一个存放数据的对象Object一个可以把我们的数据映射到HTML页面上的“模板解析”工具一个更新数据的方法一个通过监听数据的变化,更新视图的方法一个挂载模板解析的HTML标签通过上面这样的思考,我们可以简单的写一下大概的方法。 class MVVM { constructor(data){ this.$option = option; const data = this._data = this.$option.data; //数据劫持 observe(data) //数据代理 proxyData(data) //编译模板 const dom = this._el = this.$option.el; complie(dom,this); //发布订阅 //连接视图和数据 //实现双向数据绑定 }}// Observe类function Observe(){}// Observe实例化函数function observe(data){ return new Observe(data);}// Compile类function Compile(){}// Compile实例化函数function compile(el){ return new Compile(el)}数据劫持我们有下面这样一个对象 ...

June 14, 2019 · 4 min · jiezi

你可能不清楚的 Vue Router 深度用法(二)

此为 Vue Router 深度用法的第二篇,主要讲述动态路由匹配用法。第一篇的链接在此: https://segmentfault.com/a/11…动态路由匹配是用于把某种模式匹配到的所有路由,全都映射到同个组件。通过给路由路径一个变量,即变成动态路由,把变化的内容赋值给变量即可。例如文章详情页是一个组件,只有一个路由,从文章列表页点进来,变化的只是文章 id 而已。将其赋予给设定的变量,然后通过 watch ‘$route’ 或者使用 beforeRouteUpdate 导航守卫监测路由变化,传递新的文章 id 获取文章详情即可。在组件里,可以通过 this.$route.params.xx 获取当前文章 id。一个路由地址可以设置多个变量,适合有分叉情况的内容。例如 path: ‘/params/:foo/:bar’从文章列表页点进来即传递路由变量,有三种方法: (1)<router-link to="/params/list/1">跳转到 /params/list/1</router-link> (2)this.$router.push({ name: ‘articles’, params: { foo: ’list’, bar: 1 } }) (3)this.$router.push({ path: ‘/params/list/1’ }) // path 不能与 params 同时使用高级匹配模式这里主要研究的是动态路由匹配的高级匹配模式,以达到合并差异不大的路由、减少路由数量的目的。高级匹配即结合简单的正则匹配方法,给予路由更多的限制和操作空间。1、可选路由参数路由参数可选,添加与否都对应同一个组件。可以在组件里使用 v-if / v-show 结合 $route.params.xx 展现不同的内容// a param can be made optional by adding “?”{ path: ‘/optional-params/:foo?’ }// 这两个链接都对应同个组件<li><router-link to="/optional-params">/optional-params</router-link></li><li><router-link to="/optional-params/foo">/optional-params/foo</router-link></li>2、精确匹配参数只有参数通过正则匹配,完全符合格式,才能会跳转。例如只有参数是数字/手机号才允许跳转。适用于对第三方不规范格式的数据进行筛选。// a param can be followed by a regex pattern in parens// this route will only be matched if :id is all numbers{ path: ‘/params-with-regex/:id(\d+)’ }// 只匹配数字<li><router-link to="/params-with-regex/123">/params-with-regex/123</router-link></li>// 只匹配手机号{ path: ‘/params-with-regex/:id(^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$)’ }<li><router-link to="/params-with-regex/abc">/params-with-regex/13800138000</router-link></li>3、匹配任意参数不对参数格式、数量进行限制,任意参数都可。// asterisk can match anything{ path: ‘/asterisk/*’ }// 这两个都是同一组件<li><router-link to="/asterisk/foo">/asterisk/foo</router-link></li><li><router-link to="/asterisk/foo/bar">/asterisk/foo/bar</router-link></li>4、部分可选参数结合可选路由参数与多路由参数,其中一部分参数可选。适用于分叉情况下不确定参数数量的情况。// make part of the path optional by wrapping with parens and add “?”{ path: ‘/optional-group/(foo/)?bar’ }// 这两个都是同一组件<li><router-link to="/optional-group/bar">/optional-group/bar</router-link></li><li><router-link to="/optional-group/foo/bar">/optional-group/foo/bar</router-link></li> ...

February 16, 2019 · 1 min · jiezi

你可能不清楚的 Vue Router 深度用法(一)

Vue Router 简单易上手,能实现大部分的需求。但是,如果在项目里需要更细致的控制路由,以实现与其同步的效果,就需要挖掘其文档里没详细提及的内容。第一章为路由元信息用途挖掘。路由元信息用途(1)验证用户身份,定义用户权限能访问的页面大部分项目,除了登录页、重置密码页、用户协议页以外,页面都需要验证用户身份进行访问。使用 Vue Router 可以配合后端进行双重验证。(登录)验证身份方法:1、给需要验证的路由对象添加 meta 字段,里面自定义一个代表验证的字段:const router = new VueRouter({ routes: [ { path: ‘/foo’, component: Foo, children: [ { path: ‘bar’, component: Bar, meta: { requiresAuth: true // 添加该字段,表示进入这个路由是需要登录的 } } ] } ]})2、在全局导航钩子里验证 requiresAuth 字段:注意事项:使用 beforeEach 在路由变化前验证。验证原理是在跳转前,访问目标路由对象的 requiresAuth 字段判断是否需要验证用户身份,如为是,检测是否有保存用户信息(即用户登录成功后前端保存的信息,例如 token)每个路由都有一个 $route.matched 数组,包含当前路由的父级路由对象和当前路由对象,在组件中可以通过 this.$route.matched 访问beforeEach 的 to 参数即目标路由对象 $route,to.matched 即是它的路由数组因此,使用 some 方法,只要路由数组里的任意路由对象需要验证身份,即进行验证验证成功跳转正确页面;失败则跳到登录页,将目标地址附在 url 的 query 里,登录成功就跳转到目标地址router.beforeEach((to, from, next) => { if (to.matched.some(record => record.meta.requiresAuth)) { if (!auth.loggedIn()) { // 没登录 next({ path: ‘/login’, query: { redirect: to.fullPath } }) } else { next() // 确保一定要调用 next() } } else { next() // 确保一定要调用 next() }})3、拦截 http 请求,验证用户身份为了防止本地保存的 token 过期,需要拦截 http 请求,为每次请求头加上 token ,然后拦截 http 响应,根据响应内容判断是否需要跳回登录页面重新登录。使用 axios 的方法如下:// http request 拦截器axios.interceptors.request.use( config => { if (auth.loggedIn()) { // 判断是否存在token,如果存在的话,则每个http header都加上token config.headers.Authorization = token ${auth.loggedIn()}; } return config; }, err => { return Promise.reject(err); });// http response 拦截器axios.interceptors.response.use( response => { return response; }, error => { if (error.response) { switch (error.response.status) { case 401: // Unauthorized // 返回 401 清除token信息并跳转到登录页面 auth.clear(); router.replace({ path: ’login’, query: { redirect: router.currentRoute.fullPath } }) } } return Promise.reject(error.response.data) // 返回接口返回的错误信息 });前端查看权限,也是配合后端进行某些页面的隐藏显示功能。一般应用于综合的办公系统,由 IT 部分配账号,不同部门的人只能看到自己负责的内容,例如行政部不会看到财务数据页面。实现方法:前端路由每个页面的 meta 对象添加 level 字段,设置 0 ~ n 级别登录成功,后台返回用户 token 的同时,返回其所属的 level 字段组件代码比较目标页面的 level 与用户 level,只显示目标 level 小于等于用户 level 的页面全局导航钩子 beforeEach 里比较目标页面的 level 与用户 level,小于等于目标 level 则正确跳转,反之取消跳转并提示权限不足上面第4步是为了防止用户直接在浏览器输入目标地址(2)其他内容控制可以控制显示路由固定的搭配,例如某个路由地址的 title 是固定的字符串、固定的欢迎语、固定的 favicon 等。在组件里通过 this.$route.meta.xxx 访问。const router = new VueRouter({ routes: [ { path: ‘/foo’, component: Foo, children: [ { path: ‘bar’, component: Bar, meta: { title: ‘标题’, message: ‘欢迎您’, requiresAuth: true // 添加该字段,表示进入这个路由是需要登录的 } } ] } ]}) ...

February 16, 2019 · 2 min · jiezi

使用 ale.js 制作一个小而美的表格编辑器(4)

今天来教大家如何使用 ale.js 制作一个小而美的表格编辑器,首先先上 gif:是不是还是有一点非常 cool 的感觉的?那么我们现在开始吧!这是我们这篇文章结束后完成的效果(如果想继续完成请访问第五篇文章):ok,那继续开始吧(本篇文章是表格编辑器系列的第四篇文章,如果您还没有看过第一篇,请访问 第一篇文章(开源中国)):首先我们需要先添加一个 Sreach 按钮(在 handleTemplateRender 函数里)://把 定义DOM基本结构 的 returnValvar returnVal = “<table><thead><tr>”//改为var returnVal = “<table><thead><button class=‘a-btn’ onclick=‘this.methods.trigSearch()’>Search</button><tr>“之后我们需要在 methods 里面添加一个 trigSearch 函数:trigSearch(){ if (this.data.isOpenSearch) { this.data.data = this.staticData.preData; this.data.isOpenSearch = false; } else { this.data.isOpenSearch = true; }}接下来,我们需要在 data 里添加一个 isOpenSearch 变量,默认为 false:isOpenSearch: false还要在 staticData 里添加一个 preData,用来存储 bookData 数据:preData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”]]之后我们要在 handleTemplateRender 函数中增加一个判断,判断是否 openSearch 开启了://把//循环遍历bookHeader数据并输出this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”;})returnVal += “</thead></tr><tbody>”;//改为//循环遍历bookHeader数据并输出this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”;})var cellId = -1;if (this.data.isOpenSearch) { //这里增加判断 returnVal += “</tr><tr>”; for (var i = 0; i < this.data.bookHeader.length; i++) { cellId++; returnVal += “<th><input data-cell=’” + cellId + “’ type=‘text’ oninput=‘this.methods.handleSearch(this, event)’ placeHolder=‘Search…’></th>”; }}returnVal += “</thead></tr><tbody>";然后我们要继续在 methods 里面添加一个名叫 handleSearch 的函数:handleSearch(el, e) { var newData = [], elVal = el.value; this.staticData.preData.forEach(function(val, i, arr) { //判断是否拥有输入的字段 if (val[e.target.dataset.cell].indexOf(elVal) !== -1) { //添加到返回对列中 newData.push(val); } }); this.data.bookData = newData;}现在我们就已经实现了搜索功能,恭喜!这是我们目前全部的 js 代码:Ale(“excel”, { template() { return this.methods.handleTemplateRender(); }, methods: { handleTemplateRender() { //定义DOM基本结构 var returnVal = “<table><thead><button class=‘a-btn’ onclick=‘this.methods.trigSearch()’>Search</button><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1, edit = this.data.edit; //循环遍历bookHeader数据并输出 this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”; }) var cellId = -1; if (this.data.isOpenSearch) { returnVal += “</tr><tr>”; for (var i = 0; i < this.data.bookHeader.length; i++) { cellId++; returnVal += “<th><input data-cell=’” + cellId + “’ type=‘text’ oninput=‘this.methods.handleSearch(this, event)’ placeHolder=‘Search…’></th>”; } } returnVal += “</thead></tr><tbody>”; //循环遍历bookData数据并输出 this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; rowId++; //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { cellId++; if (rowId === edit.row && cellId === edit.cell) { returnVal += “<td><form data-cell=’” + cellId + “’ data-row=’” + rowId + “’ onsubmit=‘this.methods.save(event)’><input type=‘text’ value=’” + val + “’></form></td>”; } else { returnVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>”; } }) returnVal += “</tr>”; }) returnVal += “</tbody></table>”; //返回DOM结构 return returnVal; }, handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e); }, changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; } }, sortList(e) { var index = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData; }, getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; } }, handleBlockOndblclick(e) { if (!this.staticData.isOpenEdit) { this.staticData.isOpenEdit = true; this.data.edit = { row: parseInt(e.target.dataset.row), cell: parseInt(e.target.dataset.cell) } } }, save(e) { e.preventDefault(); var input = e.target.firstChild; this.staticData.isOpenEdit = false; this.data.edit = { row: -1, cell: -1 } this.data.bookData[e.target.dataset.row][e.target.dataset.cell] = input.value; this.data.bookData = this.data.bookData; }, trigSearch() { if (this.data.isOpenSearch) { this.data.bookData = this.staticData.preData; this.data.isOpenSearch = false; } else { this.data.isOpenSearch = true; } }, handleSearch(el, e) { var newData = [], elVal = el.value; this.staticData.preData.forEach(function(val, i, arr) { if (val[e.target.dataset.cell].indexOf(elVal) !== -1) { newData.push(val); } }); this.data.bookData = newData; } }, data: { bookHeader: [ “Book”, “Author”, “Language”, “Published”, “Sales” ], bookData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ], edit: { row: -1, cell: -1 }, isOpenSearch: false }, staticData: { sortBy: -1, sortType: ‘down’, isOpenEdit: false, preData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ] } }) Ale.render(“excel”, { el: “#app” })如果想了解更多,欢迎关注我在明天推出的第五篇教程,同时也关注一下 alejs 哦,感谢各位!(非常重要:如果有能力的话不妨去 Github 或 码云 上 star 一下我们吧!不过如果您特别喜欢 alejs 的话也可以 watch 或 fork 一下哦!十分感谢!) ...

January 21, 2019 · 4 min · jiezi

使用 ale.js 制作一个小而美的表格编辑器(3)

今天来教大家如何使用 ale.js 制作一个小而美的表格编辑器,首先先上 gif:是不是还是有一点非常 cool 的感觉的?那么我们现在开始吧!这是我们这篇文章结束后完成的效果(如果想继续完成请访问第四篇文章):ok,那继续开始吧(本篇文章是表格编辑器系列的第三篇文章,如果您还没有看过第一篇,请访问 第一篇文章(开源中国)):首先让我们把每一个列表项都添加一个他们的行数和列数作为 dataset 数据吧!先创建一个 rowId 变量://在 handleTemplateRender 函数里,我们把:var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy;//改为var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1;然后再在 “循环遍历bookData数据并输出” 这行注释所对应的forEach函数的里面创建一个名叫 cellId 的变量:(就是输出td标签的forEach)//原来的代码this.data.bookData.forEach(function(thisBook, i, arr) { //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //输出一列 returnVal += “<td>” + val + “</td>”; }) returnVal += “</tr>”;})//改为this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; //这里增加了一行代码 //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //输出一列 returnVal += “<td>” + val + “</td>”; }) returnVal += “</tr>”;})当然这样还没完,我们还需要改为这样:this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; //这里让rowId++ rowId++; returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //这里让cellId++ cellId++; //注意这里写了 dataset returnVal += “<td data-row=’” + rowId + “’ data-cell=’” + cellId + “’>” + val + “</td>”; }) returnVal += “</tr>”;})这样你就可以看到在控制台上已经输出了它们的 dataset:接下来,让我们往 data 里面添加一个名叫 edit 的对象,用来指定我们点击的到底是哪个表格:edit: { row: -1, //默认为-1,因为没有选中表格 cell: -1}然后,我们把下面这行代码,给他添加一个 ondblclick:returnVal += “<td data-row=’” + rowId + “’ data-cell=’” + cellId + “’>” + val + “</td>”;//改为newVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>";然后我们在 methods 对象里面添加一个 handleBlockOndblclick 的函数:handleBlockOndblclick(e) { if (!this.staticData.isOpenEdit) { //判断是否开启了edit this.staticData.isOpenEdit = true; //获取并设置目标格位置 this.data.edit = { row: parseInt(e.target.dataset.row), cell: parseInt(e.target.dataset.cell) } }}因为在 handleBlockOndblclick 函数里面,我们用到了静态数据的 isOpenEdit,所以我们需要定义一个:isOpenEdit: falseok,那么之后我们需要再改进一下输出 book 数据的那一行代码,把他改成这样:thisBook.forEach(function(val, i, arr) { cellId++; if (rowId === edit.row && cellId === edit.cell) { returnVal += “<td><form data-cell=’” + cellId + “’ data-row=’” + rowId + “’ onsubmit=‘this.methods.save(event)’><input type=‘text’ value=’” + val + “’></form></td>”; } else { returnVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>”; }})接下来让我们在上方定义一个名叫 edit 的变量吧://把var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1;//改为var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1, edit = this.data.edit;之后我们还需要在 methods 里添加一个 save 函数,用来保存修改后的结果:save(e) { e.preventDefault(); var input = e.target.firstChild; this.staticData.isOpenEdit = false; this.data.edit = { row: -1, cell: -1 } this.data.bookData[e.target.dataset.row][e.target.dataset.cell] = input.value; this.data.bookData = this.data.bookData;}好了,那么现在我们的编辑器就可以正式运作了,我们已经实现了本篇文章最开始时所做的功能!(按回车可以保存修改结果)这是我们目前全部的 js 代码:Ale(“excel”, { template() { return this.methods.handleTemplateRender(); }, methods: { handleTemplateRender() { //定义DOM基本结构 var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1, edit = this.data.edit; //循环遍历bookHeader数据并输出 this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”; }) returnVal += “</thead></tr><tbody>”; //循环遍历bookData数据并输出 this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; rowId++; //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { cellId++; if (rowId === edit.row && cellId === edit.cell) { returnVal += “<td><form data-cell=’” + cellId + “’ data-row=’” + rowId + “’ onsubmit=‘this.methods.save(event)’><input type=‘text’ value=’” + val + “’></form></td>”; } else { returnVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>”; } }) returnVal += “</tr>”; }) returnVal += “</tbody></table>”; //返回DOM结构 return returnVal; }, handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e); }, changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; } }, sortList(e) { var index = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData; }, getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; } }, handleBlockOndblclick(e) { if (!this.staticData.isOpenEdit) { this.staticData.isOpenEdit = true; this.data.edit = { row: parseInt(e.target.dataset.row), cell: parseInt(e.target.dataset.cell) } } }, save(e) { e.preventDefault(); var input = e.target.firstChild; this.staticData.isOpenEdit = false; this.data.edit = { row: -1, cell: -1 } this.data.bookData[e.target.dataset.row][e.target.dataset.cell] = input.value; this.data.bookData = this.data.bookData; } }, data: { bookHeader: [ “Book”, “Author”, “Language”, “Published”, “Sales” ], bookData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ], edit: { row: -1, cell: -1 } }, staticData: { sortBy: -1, sortType: ‘down’, isOpenEdit: false }})Ale.render(“excel”, { el: “#app”})如果想了解更多,欢迎关注我在明天推出的第四篇教程,同时也关注一下 alejs 哦,感谢各位!(非常重要:如果有能力的话不妨去 Github 或 码云 上 star 一下我们吧!不过如果您特别喜欢 alejs 的话也可以 watch 或 fork 一下哦!十分感谢!) ...

January 17, 2019 · 4 min · jiezi

使用 ale.js 制作一个小而美的表格编辑器(2)

今天来教大家如何使用 ale.js 制作一个小而美的表格编辑器,首先先上 gif:是不是还是有一点非常 cool 的感觉的?那么我们现在开始吧!这是我们这篇文章结束后完成的效果(如果想继续完成请访问第三篇文章):ok,那继续开始吧(本篇文章是表格编辑器系列的第二篇文章,如果您还没有看过第一篇,请访问 第一篇文章(开源中国)):首先我们写一个名叫 staticData 的 object,里面添加2个属性,分别是 sortBy 和 sortType:(关于 staticData 这里不做阐述,如果有需要请访问 cn.alejs.org)staticData: { sortBy: -1, //排序列索引,默认没有,所以为-1 sortType: ‘down’ //排序类型,默认为降序}之后我们在 th 标签里面增加一个 onclick 属性,指向 methods 里面的 handleTheadOnclick 函数,并传递一个 event 作为参数:(之前的代码)this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th>” + val + “</th>”;})(改进后的代码)this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + “</th>”;})为了让他显示排序时的小箭头,我们需要再改进这行代码为这样:this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”;})由于 sortBy 变量和 getSortSign 函数变量还未定义,所以我们要在之前的代码里引用一下:(原来的代码)var returnVal = “<table><thead><tr>";(改进后的代码)var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy;其中,sortBy 变量指向的是静态 data 里的 sortBy 变量,这个我们已经定义了,所以先不管他。而另一个 getSortSign 函数还没有定义,所以我们在 methods 里面定义一下他:getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; }}其功能主要就是判断是正序还是倒叙,并分别输出正反小箭头。之后我们就需要完成 handleTheadOnclick 函数了。它分别引用了 changeSortType 和 sortList 函数:handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e);}其中 changeSortType 函数是用来改变排序类型的,而 sortList 函数使用来排序的。那么我们先完成 changeSortType 函数吧:changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; }}ok,这个函数的功能和实现都非常简单,其中 cellIndex 是用来获取这是属于表格中那一列的。那么 sortList 函数的实现则稍微有些复杂:sortList(e) { //获取列索引值 var index = e.target.cellIndex; //判断排序类型 if (this.staticData.sortType === “up”) { //使用数组的 sort 函数进行排序,分别按 charCode 进行排序 this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData;}这是我们目前的全部 js 代码:Ale(“excel”, { template() { return this.methods.handleTemplateRender(); }, methods: { handleTemplateRender() { //定义DOM基本结构 var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy; //循环遍历bookHeader数据并输出 this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”; }) returnVal += “</thead></tr><tbody>”; //循环遍历bookData数据并输出 this.data.bookData.forEach(function(thisBook, i, arr) { //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //输出一列 returnVal += “<td>” + val + “</td>”; }) returnVal += “</tr>”; }) returnVal += “</tbody></table>”; //返回DOM结构 return returnVal; }, handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e); }, changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; } }, sortList(e) { var index = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData; }, getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; } } }, data: { bookHeader: [ “Book”, “Author”, “Language”, “Published”, “Sales” ], bookData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ] }, staticData: { sortBy: -1, sortType: ‘down’ }})Ale.render(“excel”, { el: “#app”})然后效果就如下图所示啦:如果想了解更多,欢迎关注我在明天推出的第三篇教程,同时也关注一下 alejs 哦,感谢各位!(非常重要:如果有能力的话不妨去 Github 或 码云 上 star 一下我们吧!不过如果您特别喜欢 alejs 的话也可以 watch 或 fork 一下哦!十分感谢!) ...

January 16, 2019 · 3 min · jiezi

浅析 web 前端 MVVM

前言记得四个月前有一次面试,面试官问我 MVVM 是什么,MVVM 的本质是什么。我大脑一片混乱,那时我对 MVVM 的认知就只是“双向绑定“和“Vue”,以这个关键字简单回答了几句,我反问 MVVM 的本质是什么,对方就重复一次双向绑定。我怎么觉得对方也没懂就随便这么一问呢…其实面试完我就急着探求 MVVM 的真谛,查了资料,做了笔记,以下是我四个月前的理解:ViewModel 和 View 是互相绑定的,我们不直对界面进行操作,只需要修改数据。而和 MVC 的区别是:MVC 的 C,接收了数据,需要手动通过 js 修改 dom,这包含了对 V 的操作而无论是 vue 还是 react,都不需要对 dom 进行操作,view 和 viewmodel 的联系显然比 mvc 里 vc 的联系紧密多了,这就是我们常说的双向绑定。我觉得是不是没有必要把 MV* 搞得这么清楚?只要知道 MVVM 的本质是双向数据绑定就好了?四个月前的我投降了,为了应付面试我依然只记得双向绑定,而且 MVC 和 MVVM 的概念依然不清晰,本质的区别还是没搞懂。不过不清晰真的很正常。因为网上关于 mvvm 和其他 mv 结构的文章不少,按道理应该不难理解,但是很多文章对 mv 的描述都不一致,这就导致很多本来就懵逼的小白更加混乱(没错就是我)。If you put ten software architects into a room and have them discuss what the Model-View-Controller pattern is, you will end up with twelve different opinions. –Josh SmithMVVM 基本信息MVVM 是一种架构模式,也被称为 model-view-binder。它由微软架构师 Ken Cooper 和 Ted Peters 开发,通过利用 WPF(微软 .NET 图形系统)和 Silverlight(WPF 的互联网应用派生品)的特性来简化用户界面的事件驱动程序设计。微软的 WPF 和 Silverlight 架构师之一John Gossman 于 2005 年在他的博客上发表了 MVVM。MVVM 结构初见MVVM 与其他两种架构的对比:MVVM:VM 在 UI 层之下。VM 为 view 暴露数据和方法,VM 推送数据到在它之下的 model。MVC:view 层在结构顶层,controller 在 view 之下。model 在 controller 之下。view 指向 controller,controller 指向 model。model 更改时 view 会得到提醒(这个情况是一个单向流)。MVP:controller 替换为 presenter。presenter 与 view 平起平坐。presenter 监听 view 和 model 的事件,作为中间人在他们之间调解两边的事件,辅助两边交流。MVVM 对于 MVC 来说更容易理解,因为 MVC 经过长久的实践,产生了很多框架,这些框架的适用领域也各有不同:有后端渲染工程、原生应用工程、前后端分离后的前端工程等,在实现 MVC 模式时理所当然地会有一定区别,这就导致了 MVC 的多样性。所以对于不同的情况,对 MVC 的理解不是完全一样的。同样的情况 MVVM 也有,作为一个较新的模式,实现比 MVC 少。此文介绍的 MVVM 模式主要以 Vue 为中心理解。MVVM 与 MVC 的对比认真看过 Vue 文档大概都能注意到,Vue 实例的变量名是 vm,文档中还很严谨地补充了一句 “虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发”。按照上面不同的工程师眼里有不同的 MVC 结构的引言,Vue 虽然“没有完全遵循 MVVM 模型”,但是我觉得这就是一种 Vue 特化的 MVVM。Vue 的 MVVMView:单文件里 <template> 标签的内容,展现给用户的内容,与 ViewModel 双向绑定,可以在其中插入 ViewModel 提供的数据。ViewModel:Vue 实例整个都是 ViewModel,与 View 双向绑定,用户在 View 修改数据或发出 ajax 等指令时, ViewModel 会及时相应,接着向下修改 Model——至此可以看出 Model 和 View 是没有直接关系的。Model:这一层或者有歧义。为了更好理解 Model 需要引入 Vuex,在有 Vuex 的情况下,Vuex 提供的数据就是 Model,这符合后端架构中 Model 包含业务逻辑的情况。但是在无 Vuex 的情况下,Model 应该就是 Vue 实例的 data 属性,也就是 JavaScript 数据对象本身。前端 MVC与之对比,MVC 的情况:View:一样是展现给用户的部分,整个或部分 HTML 页面。Model:JavaScript 的变量数据(可以包含 ajax 获取数据的逻辑,或是一个数据管理机制),但是在这里要额外地添加提醒 View 更新的机制。几个月前我还迷糊为什么 MVC 也有观察者模式,MVC 的观察者是 View,在 Model 注册为观察者就能在 Model 更新时更新。Controller:用户操作逻辑放置点,输入是用户的操作,输出是对 Model 的修改。那么问题来了:MVC 和 MVVM 都用了观察者模式,两者有何不同?看图说话:在理解 MVVM 和 MVC 的区别时我纠结了很久,基于 Vue 来说,感觉非常像 MVC:页面订阅数据;数据更新时页面更新,但是看了这幅图后豁然开朗。图中对比的是 MVC 和 MVP,但是 MVP 和 MVVM 的区别基本就是 MVVM 把三者间的操作自动绑定了,不用开发者操心 V 和 P 之间的相互操作。MVC 是由 M 通知 V,但 MVVM 是 M 通知 VM(M 和 V 没有直接关系)。拓展:React 只是 MVC 的 V?至今还广泛流传这这么一句话:React不是一个MVC框架,而是一个用于构建组件化UI的库,是一个前端界面开发工具。这不是错的,但肯定是过时的。在 React 刚面世的时候,开发团队强调了这种新型的界面便携方法(Jsx 使用函数生成界面),强调 Virtual DOM 和 diff 算法,而后来,官网已经把相关描述修改了。理解、交流进步不能缺少交流,如果大家对三种架构模式的区别有不同见解,请一定要在评论区留言。文中若出现错误观点也请提醒,谢谢热爱编程的大家。参考文献https://zh.wikipedia.org/wiki…https://russelleast.wordpress...https://medium.com/javascript…https://web.archive.org/web/20150219153055/http://joel.inpointform.net/software-development/mvvm-vs-mvp-vs-mvc-the-differences-explained/ ...

January 8, 2019 · 2 min · jiezi

defineProperty详解

可以说defineProperty是被mvvm框架激活的,虽然es5就有了但说实话了解和用的人不多下边我们来具体聊聊先从defineProperty开始说起defineproperty 操作object属性//defineproperty 有个定义object属性的功能,应该没几个人用,因为相对于obj,a = 1这种方式简直不能再难用。//通常我们定义obj属性let obj = { a:1}obj.b = 2obj[‘c’] = 3console.log(obj)//{a: 1, b: 2,c: 3} Object.defineProperty(obj,’d’,{ value: 4})console.log(obj)//{a: 1, b: 2,c: 3,d:4} //defineProperty可以定义对象属性//也可以修改Object.defineProperty(obj,‘b’,{ value: 5})console.log(obj)//{a: 1, b: 5, c: 3, d: 4}//对你没看错defineProperty有这个功能,不知可以定义新的属性还可以修改,这么逆天难用的功能为什么还要造出来?说这个有什么用?别急往下看descriptor详解defineProperty 接收三个参数object (必须有 操作的对象本身 这个很容易理解不传它操作谁?)propertyname (必须有 属性名 添加修改属性得有属性名)descriptor (必须有 官方说的我理解不了,我理解的是 属性描述1、简单点就是 设置属性的值value,2、是否可操作属性值 writable,3、是否可修改配置configurable如果值为false descriptor内的属性都不可操作)4、是否可枚举enumerable*descriptor内配置可有可无,value默认undefind,其余默认为false先做了介绍我们下边来证明下writable//栗子还是这个栗子 let obj = { a: 1 } Object.defineProperty(obj, ‘b’, { value: 2, writable: false//不可修改 }) obj.b = 3 console.log(obj) //{a: 1, b: 2} 还真是不可以 //难道是姿势不对? Object.defineProperty(obj, ‘b’, { value: 3 }) console.log(obj)//{a: 1, b: 2} 一样的效果 和姿势无关。configurable//configurable 这个比较厉害 控制descriptor内属性都不可改变不知道是不是真的//还是这个栗子 let obj = { a: 1 } Object.defineProperty(obj, ‘b’, { value: 2, //writable: false//不可修改 configurable: false }) obj.b = 5 console.log(obj)//[1,2]enumerable对否可枚举 let obj = { a: 1 } Object.defineProperty(obj, ‘b’, { value: 2, //writable: false//不可修改 //configurable: false enumerable: false }) //obj.b = 5 console.log(Object.keys(obj))//[“a”]接了下来说到重点: set和get这也是vue3.0前observe的实现原理let obj = { a: 1 } let newValue = 45 Object.defineProperty(obj, ‘b’, { get(value) { console.log(‘获取’) newVlaue = value }, set() { console.log(‘设置’) return newValue } }) obj.b = 6 //设置 obj.b //获取**写到这突然写不下去了,没有实际的应用,所以我决定延伸一篇 vue双向绑定实现。稍后跟上。。。 ...

January 4, 2019 · 1 min · jiezi

编程--基本概念

1.面向过程(PROCEDURE ORIENTED)1).具体化,流程化2).性能高3).算法+数据结构2.面向对象(OBJECT ORIENTED)(OO)1).模型化2).易维护,易复用,易扩展3.面向对象编程(OOP)1).继承 允许在现存的组件基础上创建子类组件,这统一并增强了多态性和封装性 A).重载(以统一的方法处理不同数据类型) 一个类的多态性表现 B).重写(方法重写) 父子类多态性体现2).封装(信息封装) 确保组件不会以不可预期的方式改变其它组件的内部状态3).多态 组件的引用和类集会涉及到其它不同类型的组件,而且引用组件所产生的结果得依据实际调用的类型4.面向切面编程(ASPECT ORIENTED PAROGRAMMING)(AOP)1).切面 项目模块中某些业务逻辑(业务需要一定共性)2).解耦,提高程序可重用性,提高开发效率5.三层架构、MVC、MVP、MVVM1).三层架构–界面层(User Interface Layer-Business Logic Layer-Data access Layer 界面–业务逻辑–数据访问) A).界面层(UIL) 与用户交互 B).业务逻辑层(BLL) 实现业务逻辑。业务逻辑具体包含:验证、计算、业务规则等 C).数据访问层(DAL) 与数据库打交道。主要实现对数据的增、删、改、查 2).MVC(Model-View-Controller 模型–视图–控制器) A).Model(模型) 业务逻辑、业务模型、业务操作、数据模型。定义了数据修改和操作的业务规则 B).View (视图) UI组件。接收Controller数据,降Model转化成UI C).Controller(控制器) 处理流入请求 D).特点 View和Model分离(1978 Trygve Reenskaug) E).流程 View⇒Controller⇒Model⇔View 3).MVP(Model-View-Presenter MVC改良模式(View与Model完全解耦)) A).Model(模型) 业务逻辑、业务模型、业务操作、数据模型。定义了数据修改和操作的业务规则 B).View (视图) UI组件。接收Controller数据,降Model转化成UI C).Presenter(控制器) 处理View背后所有UI事件(一个Presenter只映射一个view) D).特点 View和Presenter双向交互(IBM的子公司Taligent提出) E).流程 View⇔Presenter⇔Model 4).MVVM(Model-View-View Model MVP中把P层削弱为VM层,部分简单的逻辑职责分给了View层) A).Model(模型) 业务逻辑、业务模型、业务操作、数据模型。定义了数据修改和操作的业务规则 B).View (视图) UI组件。接收Controller数据,降Model转化成UI C).View Model(控制器) 负责暴漏方法,命令,其他属性来操作View的状态,触发View自己的事件 D).特点 View和View Model双向数据绑定关系 E).流程 View⇒View Model⇔Model ...

December 26, 2018 · 1 min · jiezi

前端技术演进(五):现代前端交互框架

这个来自之前做的培训,删减了一些业务相关的,参考了很多资料(参考资料列表),谢谢前辈们,么么哒 ????随着前端技术的发展,前端框架也在不断的改变。操作DOM时代DOM(Document Object Model,文档对象模型)将 HTML 文档表达为树结构,并定义了访问和操作 HTML 文档的标准方法。前端开发基本上都会涉及到HTML页面,也就避免不了和DOM打交道。最早期的Web前端,就是一个静态的黄页,网页上的内容不能更新。慢慢的,用户可以在Web页面上进行一些简单操作了,比如提交表单,文件上传。但是整个页面的部分或者整体的更新,还是靠刷新页面来实现的。随着AJAX技术的出现,前端页面上的用户操作越来越多,越来越复杂,所以就进入了对DOM元素的直接操作时代。要对DOM元素操作,就要使用DOM API,常见的DOM API有:类型方法节点查询getElementById、getElementsByName、getElementsByClassName、getElementsByTagName、querySelector、querySelectorAll节点创建createElement、createDocumentFragment、createTextNode、cloneNode节点修改appendChild、replaceChild、removeChild、insertBefore、innerHTML节点关系parentNode、previousSibling、childNodes节点属性innerHTML、attributes、getAttribute、setAttribure、getComputedStyle内容加载XMLHttpRequest、ActiveX使用DOM API可以完成前端页面中的任何操作,但是随着网站应用的复杂化,使用原生的API非常低效。所以 jQuery 这个用来操作DOM的交互框架就诞生了。jQuery 为什么能成为在这个时代最流行的框架呢?主要是他帮前端开发人员解决了太多问题:封装了DOM API,提供了统一和方便的调用方式。简化了元素的选择,可以很快的选取到想要的元素。提供了AJAX接口,对XMLHttpRequest和ActiveX统一封装。统一了事件处理。提供异步处理机制。兼容大部分主流浏览器。除了解决了上面这些问题,jQuery还拥有良好的生态,海量的插件拿来即用,让前端开发比以前流畅很多。尤其是在IE6、IE7时代,没有jQuery,意味着无穷的兼容性处理。// DOM API:document.querySelectorAll(’#container li’);// jQuery$(’#container’).find(’li’);随着HTML5技术的发展,jQuery提供的很多方法已经在原生的标准中实现了,慢慢的,jQuery的必要性在逐渐降低。http://youmightnotneedjquery.com/渐渐地,SPA(Single Page Application,单页面应用)开始被广泛认可,整个应用的内容都在一个页面中并完全通过异步交互来加载不同的内容,这时候使用 jQuery 直接操作DOM的方式就不容易管理了,页面上事件的绑定会变得混乱,在这种情况下,迫切需要一个可以自动管理页面上DOM和数据之间交互操作的框架。MV* 模式MVC,MVP和MVVM都是常见的软件架构设计模式(Architectural Pattern),它通过分离关注点来改进代码的组织方式。单纯从概念上,很难区分和感受出来这三种模式在前端框架中有什么不同。我们通过一个例子来体会一下:有一个可以对数值进行加减操作的组件:上面显示数值,两个按钮可以对数值进行加减操作,操作后的数值会更新显示。Model层用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法。这里我们把需要用到的数值变量封装在Model中,并定义了add、sub、getVal三种操作数值方法。var myapp = {}; // 创建这个应用对象myapp.Model = function() { var val = 0; // 需要操作的数据 /* 操作数据的方法 / this.add = function(v) { if (val < 100) val += v; }; this.sub = function(v) { if (val > 0) val -= v; }; this.getVal = function() { return val; };};View作为视图层,主要负责数据的展示。myapp.View = function() { / 视图元素 / var $num = $(’#num’), $incBtn = $(’#increase’), $decBtn = $(’#decrease’); / 渲染数据 / this.render = function(model) { $num.text(model.getVal() + ‘rmb’); };};这里,通过Model&View完成了数据从模型层到视图层的逻辑。但对于一个应用程序,这远远是不够的,我们还需要响应用户的操作、同步更新View和Model。前端 MVC 模式MVC(Model View Controller)是一种很经典的设计模式。用户对View的操作交给了Controller处理,在Controller中响应View的事件调用Model的接口对数据进行操作,一旦Model发生变化便通知相关视图进行更新。Model层用来存储业务的数据,一旦数据发生变化,模型将通知有关的视图。// Modelmyapp.Model = function() { var val = 0; this.add = function(v) { if (val < 100) val += v; }; this.sub = function(v) { if (val > 0) val -= v; }; this.getVal = function() { return val; }; / 观察者模式 / var self = this, views = []; this.register = function(view) { views.push(view); }; this.notify = function() { for(var i = 0; i < views.length; i++) { views[i].render(self); } };};Model和View之间使用了观察者模式,View事先在此Model上注册,进而观察Model,以便更新在Model上发生改变的数据。View和Controller之间使用了策略模式,这里View引入了Controller的实例来实现特定的响应策略,比如这个栗子中按钮的 click 事件:// Viewmyapp.View = function(controller) { var $num = $(’#num’), $incBtn = $(’#increase’), $decBtn = $(’#decrease’); this.render = function(model) { $num.text(model.getVal() + ‘rmb’); }; / 绑定事件 / $incBtn.click(controller.increase); $decBtn.click(controller.decrease);};控制器是模型和视图之间的纽带,MVC将响应机制封装在Controller对象中,当用户和应用产生交互时,控制器中的事件触发器就开始工作了。// Controllermyapp.Controller = function() { var model = null, view = null; this.init = function() { / 初始化Model和View / model = new myapp.Model(); view = new myapp.View(this); / View向Model注册,当Model更新就会去通知View啦 / model.register(view); model.notify(); }; / 让Model更新数值并通知View更新视图 */ this.increase = function() { model.add(1); model.notify(); }; this.decrease = function() { model.sub(1); model.notify(); };};这里我们实例化View并向对应的Model实例注册,当Model发生变化时就去通知View做更新。可以明显感觉到,MVC模式的业务逻辑主要集中在Controller,而前端的View其实已经具备了独立处理用户事件的能力,当每个事件都流经Controller时,这层会变得十分臃肿。而且MVC中View和Controller一般是一一对应的,捆绑起来表示一个组件,视图与控制器间的过于紧密的连接让Controller的复用性成了问题,如果想多个View共用一个Controller该怎么办呢?前端 MVP 模式MVP(Model-View-Presenter)是MVC模式的改良。和MVC的相同之处在于:Controller/Presenter负责业务逻辑,Model管理数据,View负责显示。在MVC里,View是可以直接访问Model的。而MVP中的View并不能直接使用Model,而是通过为Presenter提供接口,让Presenter去更新Model,再通过观察者模式更新View。与MVC相比,MVP模式通过解耦View和Model,完全分离视图和模型使职责划分更加清晰;由于View不依赖Model,可以将View抽离出来做成组件,它只需要提供一系列接口提供给上层操作。// Modelmyapp.Model = function() { var val = 0; this.add = function(v) { if (val < 100) val += v; }; this.sub = function(v) { if (val > 0) val -= v; }; this.getVal = function() { return val; };};Model层依然是主要与业务相关的数据和对应处理数据的方法,很简单。// Viewmyapp.View = function() { var $num = $(’#num’), $incBtn = $(’#increase’), $decBtn = $(’#decrease’); this.render = function(model) { $num.text(model.getVal() + ‘rmb’); }; this.init = function() { var presenter = new myapp.Presenter(this); $incBtn.click(presenter.increase); $decBtn.click(presenter.decrease); };};MVP定义了Presenter和View之间的接口,用户对View的操作都转移到了Presenter。比如这里的View暴露setter接口(render方法)让Presenter调用,待Presenter通知Model更新后,Presenter调用View提供的接口更新视图。// Presentermyapp.Presenter = function(view) { var _model = new myapp.Model(); var _view = view; _view.render(_model); this.increase = function() { _model.add(1); _view.render(_model); }; this.decrease = function() { _model.sub(1); _view.render(_model); };};Presenter作为View和Model之间的“中间人”,除了基本的业务逻辑外,还有大量代码需要对从View到Model和从Model到View的数据进行“手动同步”,这样Presenter显得很重,维护起来会比较困难。如果Presenter对视图渲染的需求增多,它不得不过多关注特定的视图,一旦视图需求发生改变,Presenter也需要改动。前端 MVVM 模式MVVM(Model-View-ViewModel)最早由微软提出。ViewModel指 “Model of View”——视图的模型。MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它View显示的数据对应的是Model哪一部分即可。我们使用Vue来完成这个栗子。在MVVM中,我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为(格式化数据由View的负责),这里可以把它理解为一个类似json的数据对象。// Modelvar data = { val: 0};和MVC/MVP不同的是,MVVM中的View通过使用模板语法来声明式的将数据渲染进DOM,当ViewModel对Model进行更新的时候,会通过数据绑定更新到View。<!– View –><div id=“myapp”> <div> <span>{{ val }}rmb</span> </div> <div> <button v-on:click=“sub(1)">-</button> <button v-on:click=“add(1)">+</button> </div></div>ViewModel大致上就是MVC的Controller和MVP的Presenter了,也是整个模式的重点,业务逻辑也主要集中在这里,其中的一大核心就是数据绑定。与MVP不同的是,没有了View为Presente提供的接口,之前由Presenter负责的View和Model之间的数据同步交给了ViewModel中的数据绑定进行处理,当Model发生变化,ViewModel就会自动更新;ViewModel变化,Model也会更新。new Vue({ el: ‘#myapp’, data: data, methods: { add(v) { if(this.val < 100) { this.val += v; } }, sub(v) { if(this.val > 0) { this.val -= v; } } }});整体来看,比MVC/MVP精简了很多,不仅仅简化了业务与界面的依赖,还解决了数据频繁更新(之前用jQuery操作DOM很繁琐)的问题。因为在MVVM中,View不知道Model的存在,ViewModel和Model也察觉不到View,这种低耦合模式可以使开发过程更加容易,提高应用的可重用性。数据绑定在Vue中,使用了双向绑定技术(Two-Way-Data-Binding),就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。其实双向数据绑定,可以简单地理解为一个模版引擎,但是会根据数据变更实时渲染。有人还不要脸的申请了专利:数据变更检测不同的MVVM框架中,实现双向数据绑定的技术有所不同。目前一些主流的实现数据绑定的方式大致有以下几种:手动触发绑定手动触发指令绑定是比较直接的实现方式,主要思路是通过在数据对象上定义get()方法和set()方法,调用时手动触发get ()或set()函数来获取、修改数据,改变数据后会主动触发get()和set()函数中View层的重新渲染功能。脏检测机制Angularjs是典型的使用脏检测机制的框架,通过检查脏数据来进行View层操作更新。脏检测的基本原理是在ViewModel对象的某个属性值发生变化时找到与这个属性值相关的所有元素,然后再比较数据变化,如果变化则进行Directive 指令调用,对这个元素进行重新扫描渲染。前端数据对象劫持数据劫持是目前使用比较广泛的方式。其基本思路是使用 Object.defineProperty 和 Object.defineProperies 对ViewModel数据对象进行属性get ()和set()的监听,当有数据读取和赋值操作时则扫描元素节点,运行指定对应节点的Directive指令,这样ViewModel使用通用的等号赋值就可以了。Vue就是典型的采用数据劫持和发布订阅模式的框架。Observer 数据监听器:负责对数据对象的所有属性进行监听(数据劫持),监听到数据发生变化后通知订阅者。Compiler 指令解析器:扫描模板,并对指令进行解析,然后绑定指定事件。Watcher 订阅者:关联Observer和Compile,能够订阅并收到属性变动的通知,执行指令绑定的相应操作,更新视图。ES6 Proxy之前我们说过 Proxy 实现数据劫持的方法:总结来看,前端框架从直接DOM操作到MVC设计模式,然后到MVP,再到MVVM框架,前端设计模式的改进原则一直向着高效、易实现、易维护、易扩展的基本方向发展。虽然目前前端各类框架也已经成熟并开始向高版本迭代,但是还没有结束,我们现在的编程对象依然没有脱离DOM编程的基本套路,一次次框架的改进大大提高了开发效率,但是DOM元素运行的效率仍然没有变。对于这个问题的解决,有的框架提出了Virtual DOM的概念。Virtual DOMMVVM的前端交互模式大大提高了编程效率,自动双向数据绑定让我们可以将页面逻辑实现的核心转移到数据层的修改操作上,而不再是在页面中直接操作DOM。尽管MVVM改变了前端开发的逻辑方式,但是最终数据层反应到页面上View层的渲染和改变仍是通过对应的指令进行DOM操作来完成的,而且通常一次ViewModel的变化可能会触发页面上多个指令操作DOM的变化,带来大量的页面结构层DOM操作或渲染。比如一段伪代码:<ul> <li repeat=“list”>{{ list.value }}</li></ul>let viewModel = new VM({ data:{ list:[{value: 1},{value: 2},{value: 3}] }})使用MVVM框架生成一个数字列表,此时如果需要显示的内容变成了 [{value: 1}, {value: 2}, {value: 3}, {value: 4}],在MVVM框架中一般会重新渲染整个列表,包括列表中无须改变的部分也会重新渲染一次。 但实际上如果直接操作改变DOM的话,只需要在<ul>子元素最后插入一个新的<li>元素就可以了。但在一般的MVVM框架中,我们通常不会这样做。毫无疑问,这种情况下MVVM的View层更新模式就消耗了更多没必要的性能。那么该如何对ViewModel进行改进,让浏览器知道实际上只是增加了一个元素呢?通过对比[{value: 1},{value: 2},{value: 3}] 和 [{value: 1}, {value: 2}, {value: 3}, {value: 4}]其实只是增加了一个 {value: 4},那么该怎样将这个增加的数据反映到View层上呢?可以将新的Model data 和旧的Model data 进行对比,然后记录ViewModel的改变方式和位置,就知道了这次View 层应该怎样去更新,这样比直接重新渲染整个列表高效得多。这里其实可以理解为,ViewModel 里的数据就是描述页面View 内容的另一种数据结构标识,不过需要结合特定的MVVM描述语法编译来生成完整的DOM结构。可以用JavaScript对象的属性层级结构来描述上面HTML DOM对象树的结构,当数据改变时,新生成一份改变后的Elements,并与原来的Elemnets结构进行对比,对比完成后,再决定改变哪些DOM元素。刚才例子里的 ulElement 对象可以理解为VirtualDOM。通常认为,Virtual DOM是一个能够直接描述一段HTMLDOM结构的JavaScript对象,浏览器可以根据它的结构按照一定规则创建出确定唯一的HTML DOM结构。整体来看,Virtual DOM的交互模式减少了MVVM或其他框架中对DOM的扫描或操作次数,并且在数据发生改变后只在合适的地方根据JavaScript对象来进行最小化的页面DOM操作,避免大量重新渲染。diff算法Virtual-DOM的执行过程:用JS对象模拟DOM树 -> 比较两棵虚拟DOM树的差异 -> 把差异应用到真正的DOM树上在Virtual DOM中,最主要的一环就是通过对比找出两个Virtual DOM的差异性,得到一个差异树对象。对于Virtual DOM的对比算法实际上是对于多叉树结构的遍历算法。但是找到任意两个树之间最小的修改步骤,一般会循环递归对节点进行依次对比,算法复杂度达到 O(n^3),这个复杂度非常高,比如要展示1000多个节点,最悲观要依次执行上十亿次的比较。所以不同的框架采用的对比算法其实是一个略简化的算法。拿React来说,由于web应用中很少出现将一个组件移动到不同的层级,绝大多数情况下都是横向移动。因此React尝试逐层的对比两棵树,一旦出现不一致,下层就不再比较了,在损失较小的情况下显著降低了比较算法的复杂度。前端框架的演进非常快,所以只有知道演进的原因,才能去理解各个框架的优劣,从而根据应用的实际情况来选择最合适的框架。对于其他技术也是如此。 ...

December 14, 2018 · 3 min · jiezi