现代web开发,大多数都遵循着视图与逻辑分离的开发原则,一反面使得代码更加易懂且易扩展,另外一方面带来的问题就是如何优雅的管理数据。于是,社区诞生了不少优秀的状态管理库,好比为React而生的Redux
,专为Vue
服务的Vuex
,还有不限定框架的Mobx
等等。在为使用这些库提高开发效率而叫好的同时,我以为咱们也应该从内部去真正的了解它们的核心原理,就好比今天这篇文章的主题依赖收集,就是其中的一个很大的核心知识。这篇文章将会带您一步一步的以最少的代码去实现一个小而美的依赖收集库,同时给您展示如何将这个库运用到小程序中去实现跨页面的状态共享。前端
依赖收集的基本原理能够归纳为如下3步:react
咱们要实现的例子:git
import { observable, observe } from "micro-reaction";
const ob = observable({
a: 1
});
observe(() => console.log(ob.a));
// logs: 1
// logs: 2
ob.a = 2;
复制代码
下面开始我将一步一步的进行实现过程讲解es6
首先,咱们须要建立一个可观察对象,其本质就是将传入的对象进行代理,而且返回这个代理对象,这里咱们使用es6
的Proxy
来修改对象的一些行为,从而实如今返回真正对象前做一些拦截操做。github
咱们定义了一个名叫observable
方法来代理对象,代码以下:web
export function observable(obj = {}) {
return createObservable(obj)
}
function createObservable(obj) {
const proxyObj = new Proxy(obj, handlers());
return proxyObj
}
复制代码
能够看到observable
方法内部就是经过new Proxy(obj,handler)
生成一个代理对象,传参分别是原始对象和代理操做方法handlers
,handlers
返回一个对象,定义了对象的原始方法,例如get
、set
,经过从新定义这两个方法,咱们能够修改对象的行为,从而完成代理操做,咱们来看看handlers
方法。小程序
function handlers() {
return {
get: (target, key, receiver) => {
const result = Reflect.get(target, key, receiver);
return result
},
set: (target, key, value, receiver) => {
const result = Reflect.set(target, key, value, receiver);
return result
}
}
}
复制代码
如上,咱们在get
和set
方法里面没有作任何操做,取值赋值操做都是原样返回。前端工程化
完成了对数据的初始定义,咱们明确下咱们的目的,咱们的最终目的是数据改变,反作用函数 effect
自动运行,而这其中的关键就是必须有个地方引用咱们建立的代理对象,从而触发代理对象内部的get
或者set
方法,方便咱们在这两个方法内部作一些依赖收集和依赖执行的工做。数组
于是,这里咱们定义了一个observe
方法,参数是一个Function
,咱们先看看这个方法的实现:数据结构
export function observe(fn) {
<!--这一行能够先忽略,后面会有介绍-->
storeFns.push(fn);
<!--Reflect.apply()就至关于fn.call(this.arguments)--> Reflect.apply(fn, this, arguments) } 复制代码
能够看到,内部执行了传入的函数,而咱们传入的函数是() => console.log(ob.a.b)
,函数执行,输出ob.a
,引用了代理对象的a
属性值,就触发了代理对象内部的get
方法。 在get
方法内部咱们就能够进行依赖收集。
function handlers() {
return {
get: (target, key, receiver) => {
const result = Reflect.get(target, key, receiver);
<!--触发依赖收集--> depsCollect({ target, key }) return result }, set: (target, key, value, receiver) => { const result = Reflect.set(target, key, value, receiver); return result } } } 复制代码
depsCollect
依赖收集方法须要作的操做就是将当前的依赖也就是() => console.log(ob.a)
这个函数fn
保存起来,那fn
怎么传过来呢?get
方法自己的入参是没有这个fn
的,回顾以前的observe
方法,这个方法有传入fn
,其中内部有个storeFns.push(fn)
这样的操做,就是经过一个数组将当前依赖函数临时收集起来。可光收集没用,咱们还要和对应的属性进行映射,以便后续某个属性变化时,咱们可以找出对应的effect
,故咱们定义了一个Map
对象来存储相应的映射关系,那须要怎样的一个映射关系呢?一个对象有多个属性,每一个属性可能都有对应的effect
,结构看起来应该是这样的:
{
obj:{
"key-1":fn1,
"key-2":fn2,
....
}
}
复制代码
咱们定义了一个全局变量storeReactions
来存储整个映射关系,它的key
是obj
,就是原始对象,obj
的值也是个Map
结构,存储了其属性和effect
的映射关系。咱们的最终目的其实也就是创建一个这样的关系。理清楚了数据存储,再来看看咱们的depsCollect
方法,其实就是将临时保存在storeFns
里面的函数取出和属性key
映射。
// 存储依赖对象
const storeReactions = new WeakMap();
// 中转数组,用来临时存储当前可观察对象的反应函数,完成收集以后当即释放
const storeFns = [];
function depsCollect({ target, key }) {
const fn = storeFns[storeFns.length - 1];
if (fn) {
const mapReactions = storeReactions.get(target);
if (!mapReactions.get(key)) {
mapReactions.set(key, fn)
}
}
}
复制代码
至此,咱们的依赖收集算是完成了,接下来就是要实现如何监听数据改变,对应effect
自动运行了。
数据变动,就是从新设置数据,相似a=2
的操做,就会触发代理对象里面的set
方法,咱们只须要在set
方法里面取出对应的effect
运行便可。
set: (target, key, value, receiver) => {
const result = Reflect.set(target, key, value, receiver);
executeReactions({ target, key })
return result
}
function executeReactions({ target, key }) {
<!-- 一时看不懂的,回顾下咱们的映射关系 -->
const mapReactions = storeReactions.get(target);
if (mapReactions.has(key)) {
const reaction = mapReactions.get(key);
reaction();
}
}
复制代码
ok,咱们的例子的实现过程讲解完了,整个实现过程仍是很清晰的,最后看看咱们的整个代码,去掉空行不到50行代码。
const storeReactions = new WeakMap(),storeFns = [];
export function observable(obj = {}) {
const proxyObj = new Proxy(obj, handlers());
storeReactions.set(obj, new Map());
return proxyObj
}
export function observe(fn) {
if (storeFns.indexOf(fn) === -1) {
try {
storeFns.push(fn);
Reflect.apply(fn, this, arguments)
} finally {
storeFns.pop()
}
}
}
function handlers() {
return {
get: (target, key, receiver) => {
depsCollect({ target, key })
return Reflect.get(target, key, receiver)
},
set: (target, key, value, receiver) => {
Reflect.set(target, key, value, receiver)
executeReactions({ target, key })
}
}
}
function depsCollect({ target, key }) {
const fn = storeFns[storeFns.length - 1];
if (fn) {
const mapReactions = storeReactions.get(target);
if (!mapReactions.get(key)) {
mapReactions.set(key, fn)
}
}
}
function executeReactions({ target, key }) {
const mapReactions = storeReactions.get(target);
if (mapReactions.has(key)) {
const reaction = mapReactions.get(key);
reaction();
}
}
复制代码
到目前为止,咱们实现的还只能观察单级的对象,若是一个对象的层级深了,相似ob.a.b
的结构,咱们的库就没法观察数据的变更,effect
也不会自动运行。那如何支持呢?核心原理就是在get
方法里面判断返回的值,若是返回的值是个对象,就递归调用observable
方法,递归调用完,接着运行observe
方法就会构建出完整的一个属性key
和反应effect
的映射关系。
function handlers() {
return {
get: (target, key, receiver) => {
const result = Reflect.get(target, key, receiver);
depsCollect({ target, key })
if (typeof result === 'object' && result != null && storeFns.length > 0) {
return observable(result)
}
return result
}
}
}
复制代码
回到ob.a.b
这样的结构,此时实际的代理对象应该是这样的{proxy(proxy(c))}
,若是这个时候咱们去修改数据,好比ob.a.b = 2
这样。
ob.a.b = 2
的运行过程会是怎样?要知道js这门语言是先编译后执行的,因此js引擎首先会去分析这段代码(编译阶段),先分析左边的表达式ob.a.b
,故先会编译ob.a
,触发了第一次get
方法,在get
方法中,result
获得的值是个对象,若是按照上述代码,又去从新观察这个对象,会致使observe
方法中构建好的映射关系丢失,其中就是对象{b:1}
中key
为b
对应的fn
丢失,由于咱们存储fn
是在observe
方法中执行的,那怎么办呢?方法是咱们应该在第一次observable
方法执行的时候,将每个key
对应的代理对象都保存起来,在赋值操做再一次触发get
方法的时候,若是已经代理过,直接返回就行,不须要从新代理。
// 存储代理对象
const storeProxys = new WeakMap();
export function observable(obj = {}) {
return storeProxys.get(obj) || createObservable(obj)
}
function createObservable(obj) {
const proxyObj = new Proxy(obj, handlers());
storeReactions.set(obj, new Map())
storeProxys.set(obj, proxyObj)
return proxyObj
}
function handlers() {
return {
get: (target, key, receiver) => {
const result = Reflect.get(target, key, receiver);
depsCollect({ target, key })
<!--若是代理存储中有某个key对应的代理直接返回便可-->
const observableResult = storeProxys.get(result);
if (typeof result === 'object' && result != null && storeFns.length > 0) {
return observable(result)
}
return observableResult || result
}
}
}
复制代码
如此,ob.a.b = 2
,控制台就会依次输出1
和2
,另外说一句,数组也是对象,故动态增长数组的值或者赋值操做都能触发响应的effect
。
const ob = observable({
a: {
b: 1,
c: []
}
});
observe(() => console.log(ob.a.c.join(", ")));
//logs: 2
ob.a.c.push(2);
复制代码
所有完整代码我已发布到个人github中,名字叫作micro-reaction,这个库彻底无依赖的,纯粹的,故能够为其它界面框架状态管理提供能量,因为小程序跨页面状态共享相关的库很少,故这里以小程序举例,如何结合micro-reaction实现跨页面状态共享。
描述下场景,有两个页面A
和B
,全局数据C
,A
和B
都引用了C
,以后,页面A
中某个交互改变了C
,A
和B
都须要自动渲染页面。结合咱们的库,C
确定是须要observable
的,observe
方法传入的fn
是会动态执行的,小程序渲染页面的方式是setData
方法,故observe
方法里面确定执行了setData()
,于是只要咱们在observe
方法里面引用C
,就会触发依赖收集,从而在下次C
改变以后,setData
方法从新运行渲染页面。
首先,咱们须要拿到每一个小程序页面的this
对象,以便自动渲染使用,故咱们须要代理Page
方法里面传入的参数,咱们定一个了mapToData
方法来代理,代码以下:
<!--全局数据-->
import homeStore from "../../store"
<!--将数据映射到页面,同时出发依赖收集,保存页面栈对象-->
import { mapToData } from "micro-reaction-miniprogram"
const connect = mapToData((store) => ({ count: store.credits.count }), 'home')
Page(connect({
onTap(e) {
homeStore.credits.count++
},
onJump(e) {
wx.navigateTo({
url: "/pages/logs/logs"
})
}
}))
复制代码
mapToData
方法返回一个函数,function mapToData(fn,name){return function(pageOpt){}}
,这里用到了闭包,外部函数为咱们传入的函数,做用是将全局数据映射到咱们的页面data
中并触发依赖收集,内部函数传入的参数为小程序页面自己的参数,里面包含了小程序的生命周期方法,于是咱们就能够在内部重写这些方法,并拿到当前页面对象并存储起来供下一次页面渲染使用。
import { STORE_TREE } from "./createStore"
import { observe, observable } from 'micro-reaction';
function mapToData(fn, name) {
return function (pageOpt) {
const { onLoad } = pageOpt;
pageOpt.onLoad = function (opt) {
const self = this
const dataFromStore = fn.call(self, STORE_TREE[name], opt)
self.setData(Object.assign({}, self.data, dataFromStore))
observe(() => {
<!--映射方法执行,触发依赖收集-->
const dataFromStore = fn.call(self, STORE_TREE[name], opt)
self.setData(Object.assign({}, self.data, dataFromStore))
})
onLoad && onLoad.call(self, opt)
}
return pageOpt
}
}
export { mapToData, observable }
复制代码
而后,页面A
改变了数据C
,observe
方法参数fn
自动执行,触发this.setData
方法,从而页面从新渲染,完整代码点击 micro-reaction-miniprogram,也能够点击查看在线Demo。
但愿个人文章可以让您对依赖收集的认识更深,以及如何触类旁通的学会使用,此外,最近在学习周爱民老师的《JavaScript核心原理解析》这门课程,其中有句话对我触动很深,引用的是金庸射雕英雄传里面的文本:教而不得其法,学而不得其道,意思就是说,传授的人没有用对方法,学习的人就不会学懂,其实我本身对学习的方法也一直都很困惑,前端发展愈来愈快,什么SSR
,什么serverless
,什么前端工程化
,什么搭建系统
各类知识概念愈来愈多,不知道该怎么学习,说不焦虑是不可能的,但坚信只有一个良好的基础,理解一些技术的本质,才能在快速发展的前端技术浪潮中,不至于被冲走,与局共勉!
最后,在贴下文章说起的两个库,欢迎star试用,提pr,感谢~
依赖收集库 micro-reaction
小程序状态管理库 micro-reaction-miniprogram