专栏首页黯羽轻扬运行时依赖收集机制

运行时依赖收集机制

一.精确数据绑定

精确数据绑定是指一次数据变化对视图的影响是可以精确预知的,不需要通过额外的检查(子树脏检查、子树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,分别是spanclass和文本内容,那么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

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

spanclass表达式getClass()求值过程中,访问data.classA时,会触发datagetter,此时执行上下文是app.getClass,那么就得到了data.classAspanclass属性有关,并且关系为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之类的解法无效

参考资料

  • vue/src/core/observer/dep.js
  • How vuejs knows the depenedencies of computed property for caching?

本文分享自微信公众号 - 前端向后(backward-fe),作者:ayqy

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-07-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ES Module

    惟一作用是让浏览代码变得容易一些,迅速找到指定模块,根本原因是单文件内容太长,已经遇到了维护的麻烦,所以手动插入一些锚点供快速跳转

    ayqy贾杰
  • 组合类型与类型保护_TypeScript笔记9

    Object.assign能把source: U身上的可枚举属性浅拷贝到target: T上,因此返回值类型为T & U

    ayqy贾杰
  • FaaS 给前端带来了什么?

    Serverless 是一种云计算理念,即无服务器计算(Serverless Computing):

    ayqy贾杰
  • Flask之路由注册(二)

    在第一节中,启动Flask的程序后,在浏览器中访问http:localhost//5000/就会显示Hello World,也就是说,在WEB的应用程序里,客户...

    无涯WuYa
  • MySQL/Oracle视图的创建与使用

    视图是一个虚拟的表,是一个表中的数据经过某种筛选后的显示方式,视图由一个预定义的查询select语句组成。

    互联网金融打杂
  • 收藏 | 10个数据科学家常犯的编程错误(附解决方案)

    数据科学家是“比软件工程师更擅长统计学,比统计学家更擅长软件工程的人”。许多数据科学家都具有统计学背景,但是在软件工程方面的经验甚少。我是一名资深数据科学家,在...

    CDA数据分析师
  • 独家 | 10个数据科学家常犯的编程错误(附解决方案)

    数据科学家是“比软件工程师更擅长统计学,比统计学家更擅长软件工程的人”。许多数据科学家都具有统计学背景,但是在软件工程方面的经验甚少。我是一名资深数据科学家,在...

    数据派THU
  • sanic(1):创建app

    sanic是一个非常NB的高性能python框架。最近正好公司有一个小项目。所以用sanic来试试手是很不错的了。 由于sanic的中文资料和项目还很少很少,...

    超级大猪
  • Python数据清洗实践

    “数据科学家们80%的精力消耗在查找、数据清理、数据组织上,只剩于20%时间用于数据分析等。”——IBM数据分析

    AI研习社
  • 微信中通过页面(H5)直接打开本地app的解决方案

    简述 微信中通过页面直接打开app分为安卓版和IOS版,两个的实现方式是完全不同的。 安卓版实现:使用腾讯的应用宝,只要配置了“微下载”之后,打开链接腾讯会帮你...

    Java中文社群_老王

扫码关注云+社区

领取腾讯云代金券