响应式前端框架

栏目: JavaScript · 发布时间: 4年前

内容简介: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下


以上所述就是小编给大家介绍的《响应式前端框架》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Computation and Programming Using Python

Introduction to Computation and Programming Using Python

John V. Guttag / The MIT Press / 2013-7 / USD 25.00

This book introduces students with little or no prior programming experience to the art of computational problem solving using Python and various Python libraries, including PyLab. It provides student......一起来看看 《Introduction to Computation and Programming Using Python》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具