此文在写作中。这篇文章意在整理自己目前对MVVM组件开发的理解。在写作过程中我发现,我自以为已经形成了对组件开发的一套理论,但其实这套理论还有很多不完善的地方。最近又翻到了波神分享-*漫谈Web前端的『组件化』*的PPT,深感要形成理论,还是需要数年的积累才行。所以这篇文章就作为我阶段性的成果,不具有太大的参考价值。
如果要给前端开发下一个定义,那就是在浏览器环境下面向对象的GUI客户端程序开发。
既然是基于面向对象的,那就要有类和对象。在Web前端开发中,最基本的类就是组件的基类(比如Vue和React的构造函数),所有的组件都是基类的一个实例。基类有属性和方法,同时还有一些生命周期钩子函数(其实就是基类上定义的一些方法,会在基类初始化的特定时刻被调用,并且允许实例重载这个方法)。
既然是GUI客户端程序开发,那Web开发中自然有着MVC之类的分层。MVC、MVP、MVVM都是经典的GUI客户端程序的设计模式[1]。目前Web开发中最流行的架构就是MVVM,MVVM已经成为了Web前端开发的一个事实标准。
谈到事实标准,客户端的开发,比如Android、iOS和Windows,都需要使用系统自带的原生UI组件为基础,配合相关的库,进行开发。
iOS丰富的原生组件
在GUI开发领域,组件是一种独立的、可复用的交互元素的封装。
Web开发最大的特点就是,因为历史原因,Web前端当初是作为展示文档的一种渠道来设计的,所以Web前端只有非常少数的几个原生组件,比如表单组件,可以使用。其他的组件都是需要开发者自行封装的。历史上出现过基于jQuery和Backbone等等框架/库的组件。这些组件在当时起到了很重要的作用。在Web标准大幅发展的今天,Web平台(浏览器)有没有提供标准的组件API呢?
答案是肯定的。目前Web前端组件的官方标准就是Web Components。Web Components标准由Custom Elements、HTML Templates、Shadow DOM和HTML Imports四部分组成。Web Components解决了组件的封装、组合以及复用问题。
但在组件的逻辑编写上,随着Web应用越来越复杂,jQuery为代表的命令式编程范式已经不能满足开发的需要。Knockout、ember、angular等MVVM框架出现后,使得声明式的编程范式成为可能。这些框架也渐渐成为了前端开发中的主流。
Web Components解决了组件的封装和复用问题,但Web Components的逻辑依然是命令式的。而且Web Components目前的浏览器兼容性还不太好,不能直接用于生产。
当前主流的Web前端框架/库Vue、React和Angular在某种程度上都提供了组件封装和声明式编程范式两个重要特性。这些框架都有自己的组件封装标准,都遵循数据驱动的范式。值得注意的是Web Components标准在某种程度上对这些框架的组件封装和组合方式有一定的影响,特别是Vuejs,在很多地方参考了Web Components,并实现了Web Components中的slot特性。在未来Web Components标准真正落地时,这些框架都可以和Web Components实现无缝的整合。
因此我们可以说,实现了声明式编程范式和组件封装、复用和组合的现代MVVM组件框架,就是目前Web前端开发的事实标准。并且这个标准在未来很长一段时间内都会持续保持稳定。
Web前端没有一套官方的原生UI组件(这里指的是官方提供的组件实现,比如日期选择组件、ListView组件等待,不是指Web Components这样的底层标准),以后也不会有,因为Web是一个开放的平台,不像其他的客户端程序的操作系统由一家公司所控制。但目前在我们的开发中,已经可以总结出一套成熟的组件。比如Ant Design中的众多组件。这些组件都是基于MVVM前端框架开发的。
前端这么多年的发展,到现在已经进入了一个比较成熟的时期了。有了成熟的模块标准和包管理系统,也有了通用的组件模型。所以假设Web也有一个官方的组件标准,那融合了Web Components和声明式编程的MVVM组件已经非常接近了。一个技术想要被称为工业级,只有形成统一的标准。Web前端的组件,虽然不会有统一的标准,但如果在MVVM组件作为事实的标准的前提下去开发,会减少很多不必要的麻烦。
写这篇博客,也是因为想总结一下目前这个时期的Web开发中的一些事实标准。相比于客户端,前端的门槛其实要更高一些,因为我们要使用堪称刀耕火种的方式去应对日益复杂的业务场景。但在当下,对于刚刚进入前端领域的同学来说,可以把一些范式作为前端的标准来学习,形成对现代前端开发的理解。在现在这个时间点进入前端行业的同学,已经没有必要再了解jQuery时代的开发范式了。
本文的题目是现代前端MVVM组件开发的基本理论,下面就分别介绍MVVM组件开发中几个关键的理论。
MVVM
Web Components并没有规定开发者应该如何去给一个组件的逻辑分层。一个组件里可能包含数据、表现(UI)和业务逻辑。在编写组件时,这些部分都需要被严格的解耦,并且规定各个部分之间的通信方式。
MVVM下,组件由如下部分构成:
组件 = 视图 + 数据 + 业务逻辑
MVVM分为View、Model和ViewModel三个部分。分别对应视图、数据和业务逻辑三部分。拿Vue举例,Vue的模板和样式属于View层。Vue的组件实例属于ViewModel,Vue的Model层,在没有引入全局Model层的情况下,就是Vue的data属性中的内容。如果开发者引入了全局的Model层,比如Redux或者MobX,那Model就是一个和Vue组件脱离的对象。
MVVM中,各个部分的关系是这样的:
组件的生命周期
客户端的组件会有生命周期函数,比如iOS的ViewController就有viewDidLoad
、viewWillDisappear
等等声明周期钩子。前端组件和客户端的的组件一样,都有着生命周期。一个前端组件在应用中,会首先初始化(创建一个新的组件实例),接着加载数据并首次渲染,然后进入一个响应数据变化并重新渲染的循环,最后如果这个组件要从应用中移除,那么组件就会被销毁。
组件在各个生命周期阶段会调用一些钩子函数,开发者如果想在组件特定的时刻执行一些逻辑,就可以在组件中实现这些钩子函数。
接下去总结一下MVVM通常都会有的生命周期。 首先,组件进入初始化阶段,在这个阶段主要就是创建组件实例,并调用init钩子函数。
然后进入初始化数据并首次渲染阶段,这个阶段,如果是Push类型的框架(关于Push和Pull在后文会提到),比如Vue,就需要对数据进行处理(对data进行递归遍历,修改getter和setter,然后调用Render Function进行依赖搜集),然后首次渲染(Vriual DOM patch)。如果是Pull类型的框架,Angular和Regular需要遍历View的AST,然后生成Watcher列表,然后进行首次的脏检查,随后View就被渲染到页面。React就启动一次渲染流程,包括调用Render Function和一次patch。最终达到的效果就是组件首次渲染到页面中。一般在此时也会有一个钩子函数被调用,开发者可以在此时执行一些需要确保UI已经渲染作为前提的逻辑。
接着进入响应数据变化并渲染阶段,这个阶段中,组件已经首次渲染了,接下来如果数据发生变化,那组件就会重新渲染,保持组件的UI和组件的状态保持同步。
如果组件的销毁方法被调用,组件就进入销毁阶段。
之所以要先说明这几个阶段,是因为理解组件的生命周期对于理解后文讲的数据侦测、渲染、模板等有着密不可分的关系。
组合
Composition over inheritence[x]。
在UI的开发中我们常常会复用一些代码。
内嵌组件
Mixin
数据驱动
这里有必要解释一下这套MVVM中,各模块之间数据的流动。View不能直接通知Model更新,而是通知ViewModel用户的交互,由ViewModel来修改Model中的数据。Model的数据变化之后,会直接触发View的更新。
此处应该有图。
最关键的一点就是,ViewModel不应该直接对View进行操作,而是应该通过修改Model中的数据,让Model驱动View进行更新。所以我们不提倡在ViewModel中进行DOM操作,因为DOM操作其实就是直接更新View层。并不是DOM操作有多低效。主要是因为既然我们采用了MVVM的范式,就应该去遵守这个范式,才能发挥出这个范式的威力。
数据变化侦测机制:Pull vs Push
这部分的内容我个人认为是非常精辟的。用Pull和Push两种方式准确的分类了现在主流的几个前端框架使用的数据变化侦测机制。
这部分的内容我第一次是听波神和我讲的,后来看了尤雨溪dotJS的演讲,也有类似的内容。
Pull类:脏检查
Angular和Regular的数据变化侦测机制属于是数据层的脏检查,React则是View层脏检查。
Push类:依赖搜集
Vue的Model记录了各个数据和不同部分View的对应关系。在ViewModel修改Model之后,Model会自动通知需要更新的View进行更新。
Pull和Push的理解
至于Pull和Push的形象理解,Pull可以理解为从需要更新的整个组件树中拉取所有的状态,和旧状态进行比对,然后去更新。Push可以理解为,框架已经知道了变化的数据,然后将更新的信号推送给需要更新的组件。
也不用太纠结Pull和Push的字面意思,总之数据变化侦测机制的区别就在于脏检查机制不知道哪些数据变了,所以需要进行数据的对比。而依赖搜集机制在数据变化那一刻就知道哪些数据变化了,也知道哪些组件依赖这些数据。
模板技术
波神对模板技术有一篇很全面的总结[x]。模板的表达能力来看,可以分为基于DSL(包括HTML)的,或者基于JavaScript的。Regular的模板是自己的DSL。Vue和React最终都是使用Render Function来生成View的AST的。也就是说Vue和React可以让开发者直接编写View结构的AST。Vue在Render Function之外提供了基于HTML的模板。这个模板的表达能力和传统的DOM based的模板是一样的。可以说是Render Function能力的一个子集。Vue让开发者自行选择使用何种技术。React的JSX因为是直接写在Render Function中的,所以很难称为是模板,只能说是一种语法糖。React只允许开发者直接编写Render Function。
渲染
在Web开发中,UI一般是用DOM或者Canvas这样的底层机制来实现的。组件的View层,实际上就是一个树结构,里面的节点是对View中元素的抽象表示。一个节点可能代表一个DOM节点,也可能代表一个组件的根节点。如果组件的View是由自定义的DSL表示的,那可能还会有其他带有语义的元素,比如if和list等等流程控制节点。
现在最流行的抽象方式是将View的结构表示为Virtual DOM树。Virtual DOM是对DOM节点的轻量级抽象表示。Render Function中可能会有一些逻辑,Virtual DOM将组件的状态传入Render Function后得到的一个树结构。所以我们就得到了那个著名的等式:
UI = f(state)
这里的UI就可以类比为Virtual DOM,函数f就是Render Function。
组件通信方式
父子组件通信
父组件到子组件 props 子组件到父组件,emit event或者父组件传callback到子组件。
非相邻组件通信
使用一个event bus。
数据层解决方案
路由
// todo
提问时间
在这篇文章中,我主要是讲了现代前端MVVM组件开发中一些重要的话题,并且对于每个话题我都对当前几个主流框架在这些方面的实现做了分类。我认为,这些可以说是目前前端开发的一个事实标准了。有了这方面的系统的知识,我们就可以回答下面的问题。我个人觉得,问一些对比类型的问题,更可以看出一个人是否有思考过自己使用的技术方案。对于市场上各个技术方案的对比和总结,就是得出一个技术体系的方法。
Q:Web Components解决了什么问题?和现代的前端框架相比有什么不同?
A:
Q:如何实现一个基于脏检查的数据绑定方案?
Q:如何实现一个Virtul DOM算法?
Q:如何实现一个基于依赖搜集的数据绑定方案?
A: 250行实现一个简单的MVVM和数据动态绑定的简单实现——基于ES5对象的getter/setter机制
Q:Vue的模板有哪些局限?如何解决?
A:HTML模板的表达能力不足,可以手写Render Function[x]。
Q:React-Redux这样的connector具体实现了什么?
A:订阅store变化,注册回调,回调中调用mapState函数。
Q:如何实现两个不相邻组件的通信?
A:
Q:JSX是模板技术吗?它和传统模板技术的区别是什么?
A:
Q:Vue和React的区别在什么地方?
A: