响应式前端框架
目录
1. 响应式前端框架1.1. 什么是响应式开发wiki上的解释
所谓响应式编程,是指不直接进行目标操作,而是用另外一种更为简洁的方式通过代理达到目标操作的目的。
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里的表格公式一样。
onAChanged(() => { b = a * 10 }) 假设我们实现了这个函数:onAChanged。你可以认为这是一个观察者,一个事件回调,或者一个订阅者。 如果用命令式(命令式和声明式)的写法来写,我们一般会写成下面这样: <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) }) 我们现在已经得到了那个漂亮公式,大家对这个公式都很熟悉了: 但是我们的应用怎么知道什么时候该重新执行这个更新函数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,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。需要主动调用。 angularjs为什么将会逐渐退出(注意不是angular),虽然目前仍然有大量的历史项目仍在使用。
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不会立即同步去调用页面渲染(不然页面就会一直在刷新了??),setState通过引发一次组件的更新过程来引发重新绘制(一个事务里). 举例: 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会被加上多少?
但如果你写个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,{ 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中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知道哪个组件确实需要被重渲染。你可以理解为每一个组件都已经自动获得了shouldComponentUpdate,但依赖收集太过细粒度的时候,也是有一定的性能开销。 1.4. MV*和组件化开发1.4.1. MV*设计MVP是MVC的变种 Presenter调用View的方法去设置界面,仍然需要大量的、烦人的代码,这实在是一件不舒服的事情。 能不能告诉View一个数据结构,然后View就能根据这个数据结构的变化而自动随之变化呢? 于是ViewModel出现了,通过双向绑定省去了很多在View层中写很多case的情况,只需要改变数据就行。(angularjs和vuejs都是典型的mvvm架构) 另外,MVC太经典了,目前在客户端(IOS,Android)以及后端仍然广泛使用。 1.4.1.1. 那么前端的MVC或者是MV*有什么问题呢?
1.4.1.2. 组件化的开发方式怎么解决的呢?组件就是: 视图 + 事件处理+ UI状态. 下图可以看到Flux要做的事,就是处理应用状态和业务逻辑 很好的实现关注点分离 1.5. 虚拟dom,模板以及jsx1.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; }
(可以看下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下 1.7. 小结响应式开发是趋势,当前各个前端框架都有自己的响应式系统实现。另外,Observable应该会加入到ES标准里,可能会在ES7+加入。 参考链接: https://medium.freecodecamp.org/is-mvc-dead-for-the-frontend-35b4d1fe39ec?gi=3d39e0be4c84#.q25l7qkpu (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |