johnzhu

1. 响应式前端框架

1.1. 什么是响应式开发

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

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不会立即同步去调用页面渲染(不然页面就会一直在刷新了

分类:

技术点:

相关文章: