响应式前端框架

1. 响应式前端框架

1.1. 什么是响应式开发

wiki上的解释前端

reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change(响应式开发是一种专一于数据流和变化传播的声明式编程范式)vue

所谓响应式编程,是指不直接进行目标操做,而是用另一种更为简洁的方式经过代理达到目标操做的目的。react

联想一下,在各个前端框架中,咱们如今要改变视图,不是用jquery命令式地去改变dom,而是经过setState(),修改this.data或修改$scope.data...jquery

1.1.1. concept

举个例子angularjs

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。redux

若是用命令式(命令式和声明式)的写法来写,咱们通常会写成下面这样:后端

<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响应。

changeDetectorInAngular.jpg

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不会当即同步去调用页面渲染(否则页面就会一直在刷新了😭),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。

setStateProcess.png

但若是你写个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()
        }
    })
}

vueObserver.png

1.3.4. 组件树的更新

react的setState
vue的this.Obj.x = xxx
angular的state.x = x

1.png

优化方法

2.png

在vue中,组件的依赖是在渲染过程当中自动追踪的,因此系统能精确知道哪一个组件确实须要被重渲染。你能够理解为每个组件都已经自动得到了shouldComponentUpdate,但依赖收集太过细粒度的时候,也是有必定的性能开销。

1.4. MV*和组件化开发

archDevelop.jpg

1.4.1. MV*设计

MVCDesign.png

MVPDesign.png

MVP是MVC的变种
View与Model不发生联系,都经过Presenter传递。Model和View的彻底解耦
View很是薄,不部署任何业务逻辑,称为“被动视图”,即没有任何主动性,而Presenter很是厚,全部逻辑都在这里。

MVVMDesign.png

Presenter调用View的方法去设置界面,仍然须要大量的、烦人的代码,这实在是一件不舒服的事情。

能不能告诉View一个数据结构,而后View就能根据这个数据结构的变化而自动随之变化呢?

因而ViewModel出现了,经过双向绑定省去了不少在View层中写不少case的状况,只须要改变数据就行。(angularjs和vuejs都是典型的mvvm架构)

另外,MVC太经典了,目前在客户端(IOS,Android)以及后端仍然普遍使用。

1.4.1.1. 那么前端的MVC或者是MV*有什么问题呢?

MVCDie.png

  • controller 和 view 层高耦合

    下图是view层和controller层在前端和服务端如何交互的,能够看到,在服务端看来,view层和controller层只两个交互。透过前端和后端的之间。

    serverMVC.png

    可是把mvc放到前端就有问题了,controller高度依赖view层。在某些框架里,甚至是被view来建立的(好比angularjs的ng-controller)。controller要同时处理事件响应和业务逻辑,打破了单一职责原则,其后果多是controller层变得愈来愈臃肿。

    clientMVC.png

  • 过于臃肿的Model层

    另外一方面,前端有两种数据状态须要处理,一个是服务端过来的应用状态,一个是前端自己的UI状态(按钮置不置灰,图标显不显示,)。一样违背了Model层的单一职责。

1.4.1.2. 组件化的开发方式怎么解决的呢?

组件就是: 视图 + 事件处理+ UI状态.

下图能够看到Flux要作的事,就是处理应用状态和业务逻辑

componentDesign.png

很好的实现关注点分离

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}}

parser.png

通过词法分析(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

Rx_Logo_S.png

响应式开发最流行的库:rxjs

Netflix,google和微软对reactivex项目的贡献很大reactivex

RxJS是ReactiveX编程理念的JavaScript版本。ReactiveX来自微软,它是一种针对异步数据流的编程。简单来讲,它将一切数据,包括HTTP请求,DOM事件或者普通数据等包装成流的形式,而后用强大丰富的操做符对流进行处理,使你能以同步编程的方式处理异步数据,并组合不一样的操做符来轻松优雅的实现你所须要的功能。

示例在demos/rxjs-demo下

1.7. 小结

响应式开发是趋势,当前各个前端框架都有本身的响应式系统实现。另外,Observable应该会加入到ES标准里,可能会在ES7+加入。

参考连接:
https://medium.com/@j_lim_j/summary-of-advanced-vuejs-features-by-evan-you-part-1-of-7-reactivity-b88ea6935a5d

https://medium.freecodecamp.org/is-mvc-dead-for-the-frontend-35b4d1fe39ec?gi=3d39e0be4c84#.q25l7qkpu

相关文章
相关标签/搜索