前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >运行时依赖收集机制

运行时依赖收集机制

作者头像
ayqy贾杰
发布2019-06-12 12:29:40
5490
发布2019-06-12 12:29:40
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

一.精确数据绑定

精确数据绑定是指一次数据变化对视图的影响是可以精确预知的,不需要通过额外的检查(子树脏检查、子树diff)来进一步确认

不妨把应用结构分为2层:

代码语言:javascript
复制
视图层
---
数据层

数据绑定就是建立数据层和视图层的联系(双向数据绑定场景还要求建立反向联系),也就是找出数据到视图的映射关系:view = f(data)。精确数据绑定是细粒度的,原子级的数据更新应该对应原子级的视图更新,例如:

代码语言:javascript
复制
<!-- 视图结构 -->
<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,分别是spanclass和文本内容,那么data.counter发生变化时,应该直接重新计算这2处,并做视图更新操作:

代码语言:javascript
复制
// 数据更新
data.counter++;
// 对应的视图更新操作
$span.className = eval("counter % 2 === 0 ? 'even' : 'odd'");
$span.textContent = eval("counter");
<!-- 更新后的视图 -->
<div id="app">
   <span class="odd">1</span>
</div>

这样的视图更新非常准确,发现数据变了立即对依赖该数据的各个表达式重新求值,并把新值同步到视图层。要想做到这种程度的准确更新,必须提前找出细粒度的精确依赖关系,类似于:

代码语言:javascript
复制
data.counter 有2处依赖该项数据,分别是
   $span.className 关系f=counter % 2 === 0 ? 'even' : 'odd'
   $span.textContent 关系f=counter

如果无法提前找出这样精确的依赖关系,就做不到精确更新,不算精确数据绑定。比如angular需要重新计算组件级的$scope下的所有属性,对比前后是否发生了变化,才能确定需要更新哪部分视图;react则需要通过组件级的向下重新计算,并做状态diff才能找出恰当的视图更新操作,再作为补丁应用到真实DOM树上。它们都不是精确数据绑定,因为数据与视图的映射关系在数据变化发生之前是未知的

想办法确定数据与视图之间的依赖关系,就是依赖收集的过程,是精确数据绑定的前提和基础

二.依赖收集

依赖收集分为2部分,编译时和运行时。前者通过静态检查(代码扫描)来发现依赖,后者通过执行代码片段根据运行时上下文来确定依赖关系

编译时依赖收集

通过扫描代码来发现依赖,比如最简单的模式匹配(或者更强大一些的语法树分析):

代码语言:javascript
复制
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

代码语言:javascript
复制
[{
   data: "counter",
   rel: "textContent",
   view: "span"
}]

这种方式相对简单,但对于表达式之类的复杂场景,靠正则匹配来收集依赖就有些不太现实了。例如:

代码语言:javascript
复制
<span bind:class="10 % 2 === 0 ? classA : classB">conditional class</span>

支持表达式的条件场景,就无法在编译时确定依赖关系,所以一般要么放弃支持这样的特性,要么放弃精确数据绑定。react选择放弃精确数据绑定,换取JSX模版支持任意JS表达式的强大特性

其实还有第三个选择,鱼和熊掌都可以要

运行时依赖收集

像上面条件class这样的例子,无法通过静态检查得到依赖关系,就只能在运行时通过执行环境来确定了

上面的例子等价于:

代码语言:javascript
复制
<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

那么问题是如何在运行时收集依赖

spanclass表达式getClass()求值过程中,访问data.classA时,会触发datagetter,此时执行上下文是app.getClass,那么就得到了data.classAspanclass属性有关,并且关系为f=app.getClass

模拟场景如下:

代码语言:javascript
复制
// 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:

代码语言:javascript
复制
// 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收集依赖:

代码语言:javascript
复制
// init view
let deps = [];let view = spanClassName;
let computedKey = view.computedKey;
let initValue = app[computedKey]();
view.value = initValue;
console.log(view);

此时将得到如下输出,表示运行时成功收集到了依赖:

代码语言:javascript
复制
classA was accessed
dep collected
Object {value: "a", computedKey: "getClass"}

接着修改数据,setter将发起重新求值,更新视图:

代码语言:javascript
复制
// update data
app.data.classA = 'newA';
// view updated automaticly
console.log(spanClassName);

得到如下日志,表示视图自动更新成功:

代码语言:javascript
复制
classA changed to newA
reeval getClass and update view
classA was accessed
Object {value: "newA", computedKey: "getClass"}

过程中没有对classB做检查或者求值,数据更新 -> 视图更新的过程没有冗余操作,非常精准

依靠这样的动态依赖收集机制,模版就可以支持任意JS表达式了,而且做到了精确的数据绑定

P.S.当然,上面的实现只是最核心的部分,运行时依赖收集机制至少还要考虑:

  • 子依赖(一个计算属性依赖另一个计算属性)
  • 依赖维护(动态添加/销毁)

同一时刻一定只有一个执行上下文(可以作为全局target),但子依赖的场景存在嵌套执行上下文,所以需要手动维护一个上下文栈(targetStack),进入计算属性求值前入栈,计算完毕出栈

三.依赖收集与缓存

有一个很经典的vue例子:

代码语言:javascript
复制
<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时(计算初始视图时),得到依赖关系:

代码语言:javascript
复制
$div.textContent - myComputed - myValue

这个关系一经确定就无法再改变,那么除非myValue变了,否则不会对myComputed重新求值,所以有了改myValue触发重新求值的解法

另一方面,既然flag的变化会影响视图,那么干脆把flag也作为myComputed的数据依赖,这就是把flag拿到data里的原因

P.S.缓存确实有一份,在赋值时setter会做脏检查,如果新值与缓存值完全相同,就不触发依赖项的重新计算,所以self.myValue = self.myValue之类的解法无效

参考资料

  • vue/src/core/observer/dep.js
  • How vuejs knows the depenedencies of computed property for caching?
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.精确数据绑定
  • 二.依赖收集
    • 编译时依赖收集
      • 运行时依赖收集
      • 三.依赖收集与缓存
        • 参考资料
        相关产品与服务
        腾讯云代码分析
        腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档