一.精确数据绑定
精确数据绑定是指一次数据变化对视图的影响是可以精确预知的,不需要通过额外的检查(子树脏检查、子树diff)来进一步确认
不妨把应用结构分为2层:
视图层
---
数据层
数据绑定就是建立数据层和视图层的联系(双向数据绑定场景还要求建立反向联系),也就是找出数据到视图的映射关系:view = f(data)。精确数据绑定是细粒度的,原子级的数据更新应该对应原子级的视图更新,例如:
<!-- 视图结构 -->
<div id="app">
    <span bind:class="counter % 2 === 0 ? 'even' : 'odd'">{{counter}}</span>
</div>
// 初始数据
app.data = {
    counter: 0,
    other: {
        /*...*/
    }
};
<!-- 初始视图 -->
<div id="app">
    <span class="even">0</span>
</div>
视图结构中有2处依赖data.counter,分别是span的class和文本内容,那么data.counter发生变化时,应该直接重新计算这2处,并做视图更新操作:
// 数据更新
data.counter++;
// 对应的视图更新操作
$span.className = eval("counter % 2 === 0 ? 'even' : 'odd'");
$span.textContent = eval("counter");
<!-- 更新后的视图 -->
<div id="app">
    <span class="odd">1</span>
</div>
这样的视图更新非常准确,发现数据变了立即对依赖该数据的各个表达式重新求值,并把新值同步到视图层。要想做到这种程度的准确更新,必须提前找出细粒度的精确依赖关系,类似于:
data.counter 有2处依赖该项数据,分别是
    $span.className 关系f=counter % 2 === 0 ? 'even' : 'odd'
    $span.textContent 关系f=counter
如果无法提前找出这样精确的依赖关系,就做不到精确更新,不算精确数据绑定。比如angular需要重新计算组件级的$scope下的所有属性,对比前后是否发生了变化,才能确定需要更新哪部分视图;react则需要通过组件级的向下重新计算,并做状态diff才能找出恰当的视图更新操作,再作为补丁应用到真实DOM树上。它们都不是精确数据绑定,因为数据与视图的映射关系在数据变化发生之前是未知的
想办法确定数据与视图之间的依赖关系,就是依赖收集的过程,是精确数据绑定的前提和基础
二.依赖收集
依赖收集分为2部分,编译时和运行时。前者通过静态检查(代码扫描)来发现依赖,后者通过执行代码片段根据运行时上下文来确定依赖关系
编译时依赖收集
通过扫描代码来发现依赖,比如最简单的模式匹配(或者更强大一些的语法树分析):
let view = '<span>{{counter}}</span>';
const REGS = {
    textContent: /<([^>\s]+).*>\s*{{([^}]*)}}\s*<\/\1>/gm
};
let deps = [];
for (let key in REGS) {
    let match = REGS[key].exec(view);
    if (match) {
        deps.push({
            data: match[2],
            view: match[1],
            rel: key
        });
    }
}
这样就得到了依赖关系deps:
[{
    data: "counter",
    rel: "textContent",
    view: "span"
}]
这种方式相对简单,但对于表达式之类的复杂场景,靠正则匹配来收集依赖就有些不太现实了。例如:
<span bind:class="10 % 2 === 0 ? classA : classB">conditional class</span>
支持表达式的条件场景,就无法在编译时确定依赖关系,所以一般要么放弃支持这样的特性,要么放弃精确数据绑定。react选择放弃精确数据绑定,换取JSX模版支持任意JS表达式的强大特性
其实还有第三个选择,鱼和熊掌都可以要
运行时依赖收集
像上面条件class这样的例子,无法通过静态检查得到依赖关系,就只能在运行时通过执行环境来确定了
上面的例子等价于:
<span bind:class="getClass()">conditional class</span>
app.getClass = () => 10 % 2 === 0 ? app.data.classA : app.data.classB;
想要知道span.className的数据依赖是classA还是classB,就得对表达式求值,即执行app.getClass()。得到span.className依赖classA这个信息后,classA发生变化时,才能根据依赖关系来更新span.className
那么问题是如何在运行时收集依赖?
对span的class表达式getClass()求值过程中,访问data.classA时,会触发data的getter,此时执行上下文是app.getClass,那么就得到了data.classA与span的class属性有关,并且关系为f=app.getClass
模拟场景如下:
// view
let spanClassName = {
    value: '',
    computedKey: 'getClass'
};
// data
let app = {
    data: {
        classA: 'a',
        classB: 'b'
    },
    getClass() {
        return 10 % 2 === 0 ? app.data.classA : app.data.classB;
    }
};
首先给数据属性挂上getter&setter,作为Subject:
// attach getter&setter to app.data
for (let key in app.data) {
    let value = app.data[key];
    Object.defineProperty(app.data, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log(`${key} was accessed`);
            if (deps.length === 0) {
                console.log(`dep collected`);
                deps.push({
                    data: key,
                    view: view,
                    rel: computedKey
                });
            }
            return value;
        },
        set(newVal) {
            value = newVal;
            console.log(`${key} changed to ${value}`);
            deps.forEach(dep => {
                if (dep.data === key) {
                    console.log(`reeval ${dep.rel} and update view`);
                    dep.view.value = app[dep.rel]();
                }
            })
        }
    })
}
然后初始化视图,对表达式求值,同时触发getter收集依赖:
// init view
let deps = [];
let view = spanClassName;
let computedKey = view.computedKey;
let initValue = app[computedKey]();
view.value = initValue;
console.log(view);
此时将得到如下输出,表示运行时成功收集到了依赖:
classA was accessed
dep collected
Object {value: "a", computedKey: "getClass"}
接着修改数据,setter将发起重新求值,更新视图:
// update data
app.data.classA = 'newA';
// view updated automaticly
console.log(spanClassName);
得到如下日志,表示视图自动更新成功:
classA changed to newA
reeval getClass and update view
classA was accessed
Object {value: "newA", computedKey: "getClass"}
过程中没有对classB做检查或者求值,数据更新 -> 视图更新的过程没有冗余操作,非常精准
依靠这样的动态依赖收集机制,模版就可以支持任意JS表达式了,而且做到了精确的数据绑定
P.S.当然,上面的实现只是最核心的部分,运行时依赖收集机制至少还要考虑:
- 子依赖(一个计算属性依赖另一个计算属性) 
- 依赖维护(动态添加/销毁) 
同一时刻一定只有一个执行上下文(可以作为全局target),但子依赖的场景存在嵌套执行上下文,所以需要手动维护一个上下文栈(targetStack),进入计算属性求值前入栈,计算完毕出栈
三.依赖收集与缓存
有一个很经典的vue例子:
<div id="app">
    <div>{{myComputed}}</div>
</div>
let flag = 1;
var runs = 0;
var vm = new Vue({
    el: "#app",
    data: {
        myValue: 'x',
        myOtherValue: 'y'
    },
    computed: {
        myComputed: function() {
            runs++;
            console.log("This function was called " + runs + " times");
            // update flag
            let self = this;
            setTimeout(function() {
                flag = 2;
                console.log('flag changed to ' + flag);
                // self.myValue = 'z';
            }, 2000)
            if (flag == 1)
                return this['my' + 'Value']
            else
                return this['my' + 'Other' + 'Value']
        }
    }
})
2秒后让flag = 2,却没有对myComputed自动重新求值,视图也没有变化
看起来像是内部缓存了一份myComputed,改了flag后用的还是缓存值,实际上是由运行时依赖收集机制决定的,与缓存机制无关。很容易发现2种解法:
- 把 - flag拿到- data里作为响应式数据
- 更新依赖的数据( - self.myValue = 'z'),触发重新求值
从运行时依赖收集的角度来看,在第一次计算myComputed时(计算初始视图时),得到依赖关系:
$div.textContent - myComputed - myValue
这个关系一经确定就无法再改变,那么除非myValue变了,否则不会对myComputed重新求值,所以有了改myValue触发重新求值的解法
另一方面,既然flag的变化会影响视图,那么干脆把flag也作为myComputed的数据依赖,这就是把flag拿到data里的原因
P.S.缓存确实有一份,在赋值时setter会做脏检查,如果新值与缓存值完全相同,就不触发依赖项的重新计算,所以self.myValue = self.myValue之类的解法无效
学习