彻底理解前端依赖收集

[广告]京东京造 K2蓝牙双模机械键盘 背光84键有线/蓝牙无线双模


依赖收集是 Vue.js 和 Mobx.js 核心的之一,那么依赖追踪算法如何工作呢?本文将带读者自己动手实现一个依赖收集的库。

早起很多双向绑定的框架,Model和Dom同步更新,或者现在较多场景Model更新时,Dom自动更新。 

例子1: 

data = {

    label: ‘深圳欢迎你’

}; 

bind(dom, data);  //实现一个bind函数,让dom和data绑定起来

data.label = “北京欢迎你”;  // dom 上显示的label就是”北京欢迎你” 

例子2: 

计算属性 sum 

data = {

    num: 3,

    price: 20,

    sum: 60    // sum = num * price    

}

那么每次data.num和data.price改变,自动更新data.sum

这两个例子用Object.defineProperty很好实现。但是耦合度很高。 

function defineReactive (obj, key, val) {

    Object.defineProperty(obj, key, {

            get () { return val },

            set (newValue) {

                // 例子1 这里要写dom更新的代码 

                // 例子2 这里要写sum更新数据的代码 

                val = newValue

            }

    })

}

也就是被依赖者要知道依赖者的存在。 那如果一个源数据被很多方依赖,那么在set部分的代码将越来越多。 

那现在 mobx 和 vuejs 的依赖收集是怎么做的呢?

我们先看个题目,计算某个同学的岁数? 

那么如何实现呢?首先,依赖于当前的年份,其次,依赖于这个同学的出生年份。 

let current = {

    year: 2018

}; 

let student = {

    born: 1995

}; 

我们需要两个函数,defineReactive(obj, key) 和 defineComputed(obj, key, computeFn, updateCallback) 来实现整个过程。 

let agedObj = {    

    age: 0   // 初始的岁数

}; 

defineReactive(current, ‘year’); 

defineReactive(student, ‘born’); 

defineComputed(agedObj, age, () => current.year – student.born, value => {

    console.log(`this student’s aged is ${value}`); 

}); 

// 标记当前正在求值的 computed 函数

let Dep = null

// 定义 computed,需传入求值函数与 computed 更新时触发的回调

function defineComputed (obj, key, computeFn, updateCallback) {

    // 封装供 reactive 收集的更新回调,以触发 computed 的更新事件

    const onDependencyUpdated = function () {

        // 在此调用 computeFn 计算出的值用于触发 computed 的更新事件

        // 供后续可能的 watch 等模块使用

        const value = computeFn()

        updateCallback(value)

    }

    Object.defineProperty(obj, key, {

        get () {

            // 标记当前依赖,供 reactive 收集

            Dep = onDependencyUpdated

            // 调用求值函数,中途收集依赖

            const value = computeFn()

            // 完成求值后,清空标记

            Dep = null

            // 最终返回的 getter 结果

            return value

        },

         // 计算属性无法 set

         set () {}

    })

}

// 通过 getter 与 setter 定义出一个 reactive

function defineReactive (obj, key) {

     let val = obj[key];

     // 在此标记哪些 computed 依赖了该 reactive

     const deps = []

     Object.defineProperty(obj, key, {

         // 为 reactive 求值时,收集其依赖

        get () {

            if (Dep) deps.push(Dep)

             // 返回 val 值作为 getter 求值结果

             return val;

         },

         // 为 reactive 赋值时,更新所有依赖它的计算属性

         set (newValue) {

             // 在 setter 中更新值

             val = newValue

             // 更新值后触发所有 computed 依赖更新

             deps.forEach(changeFn => changeFn())

             }

        })

}

其实原理很简单,就是defineComputed(obj, key, computeFn, updateCallback) 在调用的时候,先把computeFn和updateCallback组成的一个新的函数赋给全局的Dep函数,因为computeFn在执行的时候回去调用依赖对象的字段的get方法。 get 方法在调用的时候,会把Dep函数作为依赖收集起来,在set的时候会把所有依赖函数调用更新。 

码中人 微信公众号