响应式前端框架
栏目: JavaScript · 发布时间: 5年前
内容简介:wiki上的解释reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change(响应式开发是一种专注于数据流和变化传播的声明式编程范式)所谓响应式编程,是指不直接进行目标操作,而是用另外一种更为简洁的方式通过代理
wiki上的解释
reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change(响应式开发是一种专注于数据流和变化传播的声明式编程范式)
所谓响应式编程,是指不直接进行目标操作,而是用另外一种更为简洁的方式通过代理 达到目标操作的目的。
联想一下,在各个前端框架中,我们现在要改变视图,不是用jquery命令式地去改变dom,而是通过setState(),修改this.data或修改$scope.data...
1.1.1. concept
举个例子
let a =3; let b= a*10; console.log(b) //30 a=4 //b = a * 10 console.log(b)//30 复制代码
这里b并不会自动根据a的值变化,每次都需要b = a * 10再设置一遍,b才会变。所以这里不是响应式的。
B和A之间就像excel里的表格公式一样。 B1的值要“响应式”地根据A1编辑的值相应地变化
A | B | |
---|---|---|
1 | 4 | 40(fx=A1*10) |
onAChanged(() => { b = a * 10 }) 复制代码
假设我们实现了这个函数:onAChanged。你可以认为这是一个观察者,一个事件回调,或者一个订阅者。 这无所谓,关键在于,只要我们完美地实现了这个方法,B就能永远是10倍的a。
如果用命令式(命令式和声明式)的写法来写,我们一般会写成下面这样:
<span class="cell b1"></span> document .querySelector(‘.cell.b1’) .textContent = state.a * 10 复制代码
把它改的声明式一点,我们给它加个方法:
<span class="cell b1"></span> onStateChanged(() => { document .querySelector(‘.cell.b1’) .textContent = state.a * 10 }) 复制代码
更进一步,我们的标签转成模板,模板会被编译成render函数,所以我们可以把上面的js变简单点。
模板(或者是jsx渲染函数)设计出来,让我们可以很方便的描述state和view之间的关系,就和前面说的excel公式一样。
<span class="cell b1"> {{ state.a * 10 }} </span> onStateChanged(() => { view = render(state) }) 复制代码
我们现在已经得到了那个漂亮公式,大家对这个公式都很熟悉了: view = render(state) 这里把什么赋值给view,在于我们怎么看。在虚拟dom那,就是个新的虚拟dom树。我们先不管虚拟dom,认为这里就是直接操作实际dom。
但是我们的应用怎么知道什么时候该重新执行这个更新函数onStateChanged?
let update const onStateChanged = _update => { update = _update } const setState = newState => { state = newState update() } 复制代码
设置新的状态的时候,调用update()方法。状态变更的时候,更新。 同样,这里只是一段代码示意。
1.2. 不同的框架中
在react里:
onStateChanged(() => { view = render(state) }) setState({ a: 5 }) 复制代码
redux:
store.subscribe(() => { view = render(state) }) store.dispatch({ type: UPDATE_A, payload: 5 }) 复制代码
angularjs
$scope.$watch(() => { view = render($scope) }) $scope.a = 5 // auto-called in event handlers $scope.$apply() 复制代码
angular2+:
ngOnChanges() { view = render(state) }) state.a = 5 // auto-called if in a zone Lifecycle.tick() 复制代码
真实的框架里肯定不会这么简单,而是需要更新一颗复杂的组件树。
1.3. 更新过程
如何实现的?是同步的还是异步的?
1.3.1. angularjs (脏检查)
脏检查核心代码
(可具体看test_cast第30行用例讲解)
Scope.prototype.$$digestOnce = function () { //digestOnce至少执行2次,并最多10次,ttl(Time To Live),可以看test_case下gives up on the watches after 10 iterations的用例 var self = this; var newValue, oldValue, dirty; _.forEachRight(this.$$watchers, function (watcher) { try { if (watcher) { newValue = watcher.watchFn(self); oldValue = watcher.last; if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) { self.$$lastDirtyWatch = watcher; watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue : oldValue), self); dirty = true; } else if (self.$$lastDirtyWatch === watcher) { return false; } } } catch (e) { // console.error(e); } }); return dirty; }; 复制代码
digest循环是同步进行。当触发了angularjs的自定义事件,如ng-click,$http,$timeout等,就会同步触发脏值检查。(angularjs-demos/twowayBinding)
唯一优化就是通过lastDirtyWatch变量来减少watcher数组后续遍历(这里可以看test_case:'ends the digest when the last watch is clean')。demo下有src
其实提供了一个异步更新的API叫$applyAsync。需要主动调用。 比如$http下设置useApplyAsync(true),就可以合并处理几乎在相同时间得到的http响应。
angularjs为什么将会逐渐退出(注意不是angular),虽然目前仍然有大量的历史项目仍在使用。
- 数据流不清晰,回环,双向 (子scope是可以修改父scope属性的,比如test_case里can manipulate a parent scope's property)
- api太复杂,黑科技
- 组件化大势所趋
1.3.2. react (调和过程)
调和代码
function reconcile(parentDom, instance, element) { //instance代表已经渲染到dom的元素对象,element是新的虚拟dom if (instance == null) { //1.如果instance为null,就是新添加了元素,直接渲染到dom里 // Create instance const newInstance = instantiate(element); parentDom.appendChild(newInstance.dom); return newInstance; } else if (element == null) { //2.element为null,就是删除了页面的中的节点 // Remove instance parentDom.removeChild(instance.dom); return null; } else if (instance.element.type === element.type) { //3.类型一致,我们就更新属性,复用dom节点 // Update instance updateDomProperties(instance.dom, instance.element.props, element.props); instance.childInstances = reconcileChildren(instance, element); //调和子元素 instance.element = element; return instance; } else { //4.类型不一致,我们就直接替换掉 // Replace instance const newInstance = instantiate(element); parentDom.replaceChild(newInstance.dom, instance.dom); return newInstance; } } //子元素调和的简单版,没有匹配子元素加了key的调和 //这个算法只会匹配子元素数组同一位置的子元素。它的弊端就是当两次渲染时改变了子元素的排序,我们将不能复用dom节点 function reconcileChildren(instance, element) { const dom = instance.dom; const childInstances = instance.childInstances; const nextChildElements = element.props.children || []; const newChildInstances = []; const count = Math.max(childInstances.length, nextChildElements.length); for (let i = 0; i < count; i++) { const childInstance = childInstances[I]; const childElement = nextChildElements[I]; const newChildInstance = reconcile(dom, childInstance, childElement); //递归调用调和算法 newChildInstances.push(newChildInstance); } return newChildInstances.filter(instance => instance != null); } 复制代码
setState不会立即同步去调用页面渲染(不然页面就会一直在刷新了:sob:),setState通过引发一次组件的更新过程来引发重新绘制(一个事务里). 源码的setState在src/isomorphic/modern/class/ReactComponent.js下(15.0.0)
举例:
this.state = { count:0 } function incrementMultiple() { const currentCount = this.state.count; this.setState({count: currentCount + 1}); this.setState({count: currentCount + 1}); this.setState({count: currentCount + 1}); } 复制代码
上面的setState会被加上多少?
在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true, 而当React在调用事件处理函数之前 就会调用这个batchedUpdates,造成的后果,就是由 React控制的事件处理过程 setState不会同步更新this.state。
但如果你写个setTimeout或者使用addEventListener添加原生事件,setState后state就会被同步更新,并且更新后,立即执行render函数。
(示例在demo/setState-demo下)
那么react会在什么时候统一更新呢,这就涉及到源码里的另一个概念事务。事务这里就不详细展开了,我们现在只要记住一点,点击事件里不管设置几次state,都是处于同一个事务里。
1.3.3. vue(依赖追踪)
核心代码:
export function defineReactive(obj, key, val) { var dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // console.log('geter be called once!') var value = val if (Dep.target) { dep.depend() } return value }, set: function reactiveSetter(newVal) { // console.log('seter be called once!') var value = val if (newVal === value || (newVal !== newVal && value !== value)) { return } val = newVal dep.notify() } }) } 复制代码
1.3.4. 组件树的更新
react的setState vue的this.Obj.x = xxx angular的state.x = x
优化方法
在vue中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知道哪个组件确实需要被重渲染。你可以理解为每一个组件都已经自动获得了shouldComponentUpdate,但依赖收集太过细粒度的时候,也是有一定的性能开销。
1.4. MV*和组件化开发
1.4.1. MV*设计
MVP是MVC的变种 View与Model不发生联系,都通过Presenter传递。Model和View的完全解耦 View非常薄,不部署任何业务逻辑,称为“被动视图”,即没有任何主动性,而Presenter非常厚,所有逻辑都在这里。
Presenter调用View的方法去设置界面,仍然需要大量的、烦人的代码,这实在是一件不舒服的事情。
能不能告诉View一个数据结构,然后View就能根据这个数据结构的变化而自动随之变化呢?
于是ViewModel出现了,通过双向绑定省去了很多在View层中写很多case的情况,只需要改变数据就行。(angularjs和vuejs都是典型的mvvm架构)
另外,MVC太经典了,目前在客户端(IOS,Android)以及后端仍然广泛使用。
1.4.1.1. 那么前端的MVC或者是MV*有什么问题呢?
-
controller 和 view 层高耦合
下图是view层和controller层在前端和服务端如何交互的,可以看到,在服务端看来,view层和controller层只两个交互。透过前端和后端的之间。
但是把mvc放到前端就有问题了,controller高度依赖view层。在某些框架里,甚至是被view来创建的(比如angularjs的ng-controller)。controller要 同时处理事件响应和业务逻辑 ,打破了单一职责原则,其后果可能是controller层变得越来越臃肿。
-
过于臃肿的Model层
另一方面,前端有两种数据状态需要处理,一个是服务端过来的 应用状态 ,一个是前端本身的 UI状态 (按钮置不置灰,图标显不显示,)。同样违背了Model层的单一职责。
1.4.1.2. 组件化的开发方式怎么解决的呢?
组件就是: 视图 + 事件处理+ UI状态.
下图可以看到Flux要做的事,就是处理应用状态和业务逻辑
很好的实现关注点分离
1.5. 虚拟dom,模板以及jsx
1.5.1. vue和react
虚拟dom其实就是一个轻量的js对象。 比如这样:
const element = { type: "div", props: { id: "container", children: [ { type: "input", props: { value: "foo", type: "text" } }, { type: "a", props: { href: "/bar" } }, { type: "span", props: {} } ] } }; 复制代码
对应于下面的dom:
<div id="container"> <input value="foo" type="text"> <a href="/bar"></a> <span></span> </div> 复制代码
通过render方法(相当于ReactDOM.render)渲染到界面
function render(element, parentDom) { const { type, props } = element; const dom = document.createElement(type); const childElements = props.children || []; childElements.forEach(childElement => render(childElement, dom)); //递归 parentDom.appendChild(dom); // ```对其添加属性和事件监听 } 复制代码
jsx
<div id="container"> <input value="foo" type="text" /> <a href="/bar">bar</a> <span onClick={e => alert("Hi")}>click me</span> </div> 复制代码
一种语法糖,如果不这么写的话,我们就要直接采用下面的函数调用写法。
babel(一种预编译工具)会把上面的jsx转换成下面这样:
const element = createElement( "div", { id: "container" }, createElement("input", { value: "foo", type: "text" }), createElement( "a", { href: "/bar" }, "bar" ), createElement( "span", { onClick: e => alert("Hi") }, "click me" ) ); 复制代码
createElement会返回上面的虚拟dom对象,也就是一开始的element
function createElement(type, config, ...args) { const props = Object.assign({}, config); const hasChildren = args.length > 0; props.children = hasChildren ? [].concat(...args) : []; return { type, props }; //...省略一些其他处理 } 复制代码
同样,我们在写vue实例的时候一般这样写:
// template模板写法(最常用的) new Vue({ data: { text: "before", }, template: ` <div> <span>text:</span> {{text}} </div>` }) // render函数写法,类似react的jsx写法 new Vue({ data: { text: "before", }, render (h) { return ( <div> <span>text:</span> {{text}} </div> ) } }) 复制代码
由于vue2.x也引入了虚拟dom,他们会先被解析函数转换成同一种表达方式
new Vue({ data: { text: "before", }, render(){ return this.__h__('div', {}, [ this.__h__('span', {}, [this.__toString__(this.text)]) ]) } }) 复制代码
这里的this. h 就和react下的creatElement方法一致。
1.5.2. js解析器:parser
最后,模板的里的表达式都是怎么变成页面结果的?
举个简单的例子,比如在angular或者vue的模板里写上{{a+b}}
经过词法分析(lexer)就会变成一些符号(Tokens)
[ {text: 'a', identifier: true}, {text: '+'}, {text: 'b', identifier: true} ] 复制代码
然后经过(AST Builder)就转化成抽象语法数(AST)
{ type: AST.BinaryExpression, operator: '+', left: { type: AST.Identifier, name: 'a' }, right: { type: AST.Identifier, name: 'b' } } 复制代码
最后经过AST Compiler变成表达式函数
function(scope) { return scope.a + scope.b; } 复制代码
- 词法分析会一个个读取字符,然后做不同地处理,比如会有peek方法,如当遇到x += y这样的表达式,处理+时会去多扫描一个字符。
(可以看下angularjs源码test_case下516行的'parses an addition',最后ASTCompiler.prototype.compile返回的函数)
1.6. rxjs
响应式开发最流行的库: rxjs
Netflix,google和微软对reactivex项目的贡献很大reactivex
RxJS是ReactiveX编程理念的JavaScript版本。ReactiveX来自微软,它是一种针对异步数据流的编程。简单来说, 它将一切数据,包括HTTP请求,DOM事件或者普通数据等包装成流的形式 ,然后用强大丰富的操作符对流进行处理,使你能以同步编程的方式处理异步数据,并组合不同的操作符来轻松优雅的实现你所需要的功能。
示例在demos/rxjs-demo下
以上所述就是小编给大家介绍的《响应式前端框架》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 前端响应式你了解多少?
- 灵雀云前端: Angular 响应式表单
- 前端基本功-响应式布局(flex)
- Materialize 0.100.2 发布,响应式前端框架
- Foundation 6.5.2 发布,响应式前端框架
- Foundation v6.5.1 发布,响应式前端框架
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JSP应用开发技术
柳永坡 / 人民邮电出版社 / 2005-9 / 52.00元
本书全面系统地介绍了JSP应用开发技术,包括JSP预备知识和环境配置、JSP编程基础、JSP应用开发进阶、在JSP中使用数据库、Servlet技术、标签库和表达式语言、Web编程模式和应用框架等几个方面的内容。本书不但由浅入深地介绍了JSP程序设计的原理、方法和技术,还提供了大量的JSP应用开发实例,给出了相应的实用技巧、操作步骤及优化思路。 本书着重于JSP技术的应用性和可操作性,......一起来看看 《JSP应用开发技术》 这本书的介绍吧!