Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue原理—依赖收集追踪的基本原理 #9

Open
mobei95 opened this issue Mar 21, 2020 · 0 comments
Open

Vue原理—依赖收集追踪的基本原理 #9

mobei95 opened this issue Mar 21, 2020 · 0 comments
Labels
Vue原理 分析Vue的实现原理

Comments

@mobei95
Copy link
Owner

mobei95 commented Mar 21, 2020

依赖收集追踪的基本原理

在前两篇文章中主要了解了模板编译机制响应式的基本原理,实现了简单的模板编译和数据响应,但是数据的改变还不能触发视图的更新

本文将主要探索vue中的依赖收集与追踪的原理,实现数据与视图的绑定关系;

Dep 订阅者

首先来实现一个订阅者Dep,它的主要作用是存放Watcher的实例

class Dep {
    constructor() {
        // 存放watcher
        this.subs = []
    }
    // 新增watcher
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 通知所有的watcher更新
    notify() {
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

在上面实现的Dep类中,主要做了两件事情:

  1. addSub方法用于向Dep类中添加watcher
  2. notify方法用于遍历所有的watcher,并调用watcherupdate方法,通知视图更新

Watcher 观察者

接下来实现一个Watcher类,它的主要作用是监听属性的更改并通知视图更新

class Watcher {
    constructor(vm, expr, cb) {
        this.vm = vm
        this.expr = expr
        this.cb = cb
        // 获取一个旧的值
        this.oldVal = this.getOldValue()
        
    }
    // 获取旧的值
    getOldValue() {
        // 将当前的watcher实例作为Dep.target属性的值
        Dep.target = this
        let oldValue = directiveUtil.getVal(this.expr, this.vm)
        Dep.target = null
        return oldValue
    }
    // 通知视图更新
    update() {
        let newVal = directiveUtil.getVal(this.expr, this.vm)
        if (newVal != this.oldVal) {
            this.cb(newVal)
        }
    }
}

在上面实现Watcher中,主要做了这几件时间

  1. getOldValue方法用于获取当前的属性值oldVal,这个值的主要作用是在update中判断更新的值与当前的值是否相等,如果相等则不更新视图
  2. directiveUtil.getVal方法是在之前的代码中实现的一个通过绑定字符串获取属性值的方法
  3. update方法用于在数据更新的时候通知视图更新
  4. cb是数据更新后的回调函数,并且将新的值作为参数返回
  5. 在实例化的时候将Dep.target设置为当前实例,并在取值完成后将它清空

到这里,已经实现了依赖收集与追踪的两个非常重要的类;

那么接下来就是收集数据的依赖;因为依赖收集和追踪的主要目的是通知视图更新,所以只需要对模板中绑定的数据进行依赖收集

模板中绑定的数据都会通过指令集中的指令方法去改变视图,所以下面对指令集directiveUtil做一些改造

directiveUtil = {
    // 获取属性的value值,eg:data.key
    getVal(expr, vm) {
        let exprs = expr.split('.')
        // console.log('exprs', exprs, vm.$data)
        return exprs.reduce((current, item) => {
            return current[item]
        }, vm.$data)
    },
    // 获取内容,eg:{{a}}{{b}}{{c}}
    getContent(expr, vm) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(args[1], vm)
        })
    },
    // 设置属性的value值
    setVal(expr, vm, value) {
        let exprs = expr.split('.')
        exprs.reduce((current, item, index, arr) => {
            if (index == arr.length - 1) {
                current[item] = value
            }
            return current[item]
        }, vm.$data)
    },
    // v-model指令的处理方法
    model(node, expr, vm) {
        let fn = this.uploadVlue.uploadModel
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal)
        })
        // 绑定事件
        addEventListener('change', (e) => {
            this.setVal(expr, vm, e.target.value)
        }, false)
        fn(node, this.getVal(expr,vm))
    },
    // v-html指令的处理方法
    html(node, expr, vm) {
        let fn = this.uploadVlue.uploadHtml
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal)
        })
        fn(node, this.getVal(expr,vm))
    },
    // 小胡子语法的处理方法
    text(node, expr, vm) {
        let fn = this.uploadVlue.uploadText
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            new Watcher(vm, args[1], () => {
                fn(node, this.getContent(expr, vm))
            })
            return this.getVal(args[1], vm)
        })
        fn(node, content)
    }
}

在上面的代码中,主要做了几个改造

  1. 将每个指令所绑定的属性都绑定到一个Watcher实例进行依赖追踪,当属性值发生改变时就会触发Watcher实例创建时的的回调函数,通过回调函数去改变视图
  2. getContent方法用于重新解析小胡子语法中的内容
  3. model方法添加了事件绑定
  4. setVal方法的功能是通过表达式设置属性值

到这里已经找到了需要进行收集的数据,那么接下来就需要对defineReactive方法进行改造,完成依赖的收集

defineReactive(obj, key, value) {
    let dep = new Dep()
    Object.defineProperty(obj, key, {
        configurable: true, // 属性可以修改和删除
        enumerable: true, // 属性可以通过for...in和Object.keys()遍历
        get() {
            // 获取属性时触发
            Dep.target && dep.addSub(Dep.target)
            return value
        },
        set(newVal) {
            // 设置属性时触发
            if (newVal != value) { // 如果新的值和旧的值一致,则没有设置的必要
                value = new Val
                dep.notify()
            }
        }
    })
}  

在上面的代码改造中,每一个属性都会创建一个独立的Dep实例,然后通过这个实例来收集watcher;在读取属性的时候,get方法会将当前watcher收集到闭包中Dep实例的subs中,在写属性值的时候,set方法则会通过闭包中的Dep实例调用notify方法来触发实例中所有watcher对象的update方法更新相应的视图

小结

为了便于理解,下面将按照代码的执行顺序来总结一下属性依赖收集与追踪的整个过程

  1. 首先需要理解的是,不是所有的属性都需要进行依赖收集,只需要对通过指令或小胡子语法绑定对视图有影响的属性;
  2. 通过指令和小胡子语法绑定的属性都将使用指令集directiveUtil中对应的方法进行视图更新
  3. 在指令的处理方法中使用当前的Vue实例,绑定的属性表达式和一个回调函数作为参数创建一个Watcher实例
  4. 在创建实例时代码执行进入Watcher内部,调用getOldValue方法获取到当前表达式的值,同时会将Dep.target的值设置为当前Watcher实例
  5. 在调用getOldValue的时候会触发属性的get方法,在get方法中,会将第3步中创建的Watcher实例push到闭包中Dep实例的subs
  6. 到这里就已经完成了属性的依赖收集过程,在下面的过程中都是依赖追踪过程
  7. 当设置属性值的时候会触发属性的set方法,set方法会调用闭包中Dep实例的notify方法,notify方法会遍历当前实例所有的watcher,并调用watcherupdate方法
  8. update方法调用的时候会判断新的属性值与旧的属性值(oldVal)是否相同,如果不相同,则会将新的属性值作为第3步中实例创建的时候传入的回调函数的参数,并且调用该函数
  9. 此时,代码的执行又回到了指令集directiveUtil中对应的指令方法中,在这里将调用视图更新的方法进行视图更新

到这里,就已经完成了依赖收集与追踪的整个过程;配合之前的两篇文章,就已经完成了整个响应式系统的原理分析;其主要就是通过Observer类实现数据的可响应,通过Compiler类进行模板编译,通过WatcherDep进行依赖收集;

本文的源代码我已经提交到我的GitHub,欢迎大佬们拍砖

end

@mobei95 mobei95 added the Vue原理 分析Vue的实现原理 label Mar 21, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Vue原理 分析Vue的实现原理
Projects
None yet
Development

No branches or pull requests

1 participant