响应式原理实现

昨天讲完响应式的基本原理,今天就动手做个简单的,然后顺便把Vue中的实现讲讲

手动造一个简单的试试水

我们先自己实现一个响应式的思路

  • _init进行依赖收集
  • defineReactive进行数据劫持函数定义
  • 触发监听事件
  • 派发更新

HTML结构

下面先写一个简单的结构用于后面的说明:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>响应式实例</title>
</head>
<body>
    <button class="test-btn">Click</button>
    <p>You have clicked <span class='sum'>0</span> </p>

    <script src="./main.js"></script>
</body>
</html>

main.js

js部分首先是_init函数

function _init () {
    // 定义数据劫持
    observeObj(reactiveObj);
    
    // 添加监听事件
    let button = document.querySelector('.test-btn')
    button.addEventListener('click', () => {
        reactiveObj.clickSum += 1
    })
    
    // 模拟Dep.target,用于标明函数执行环境,介绍见Vue实现解析
    currentTarget = 'window'
    // 模拟Vue的更新
    fakeUpdateComponentFunc()
    // 恢复Dep.target
    currentTarget = ''
}

数据劫持实现

对对象的属性进行遍历,通过Object.defineProperty进行劫持函数的编写

function observeObj (reactiveObj) {
    for (let key in reactiveObj) {
        defineReactive(reactiveObj, key)
    }
}
function defineReactive (obj, key) {
    let val = obj[key]
    let dep = ''

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () { },
        set: function reactiveSetter (newValue) {  }
    })
}

重点介绍下getter和setter函数

Object.defineProperty(obj, key, {
    enumerable: true, // 可枚举
    configurable: true, // 可配置
    get: function reactiveGetter () {
        console.log('getter has been called')

        // 这里进行依赖收集
        // 判断当前的执行环境,将dep保存给setter更新时用
        if (currentTarget) {
            // dep.depend()
            console.log('===> dep collection done')
        }

        return val
    },
    set: function reactiveSetter (newValue) {
        console.log('setter has been called')

        if (newValue === val) {
            return
        }
        console.log('===> value has been set')
        val = newValue
        
        // 这里进行派发更新,其实notify就会执行updateComponent,这里做了省略
        // dep.notify()
        fakeUpdateComponentFunc()
    }
})

我们在来看看updateComponent函数,这里模拟了vue.$mount过程中执行的两个关键函数

function fakeUpdateComponentFunc () {
    (function _render() {
        // do nothing...
    })(); // template/render func => vnode
    
    (function _update() {
        applyToDom('.sum', reactiveObj.clickSum)
    })() // vnode => dom
}

然后就完啦,一个简单的响应式系统就实现了233,可以运行下看看效果哈

Vue中的响应式实现

看完上面的解释是不是感觉条理清晰很多呀,基本上Vue的实现思路即是如此,那么我们来看看源代码中的执行思路

我们先来看看几个基本概念

Dep

源码中的注释是这么写的

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */

// dep是一个能被多个指令订阅的观察者...

Dep即Dependences,在Vue中是用一个类来实现的,拥有上面依赖收集和派发更新的两个核心方法 depend 和 notify

dep.depend

depend () {
    if (Dep.target) {
        Dep.target.addDep(this)
    }
}

Dep.target是一个Dep类的静态属性,是全局唯一的,代表了当前执行的Watcher

这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组

dep.notify

notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
    }
}

subs就是各个Watcher,这里调用他们的update方法,就是通知各个watcher进行刷新,类似demo中的updateComponent

Watcher

Watcher也是一个class,构造函数中定义了一些与Dep相关的属性

class Watcher {
    constructor() {
        // ...
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
    }
    // Evaluate the getter, and re-collect dependencies
    get() {}
    /**
    * Add a dependency to this directive.
    */
    addDep() {}
    /**
    * Clean up for dependency collection.
    */
    cleanupDeps() {}

}

其功能就是做一些判断,然后将自己在依赖收集时加入dep

Dep和Watcher

Dep 实际上就是对 Watcher 的一种管理,Dep 脱离 Watcher 单独存在是没有意义的

我们可以发现,我在demo中是没有用到Dep和Watcher的这个概念的,那为啥Vue使用到了呢?

TIP

主要原因还是在demo中,reactiveObj只有一个被监听的数据项

Vue是数据驱动的,每当数据变化都会重新render,那么vm._render()方法又会再次执行,并触发getters

为了避免重复订阅watcher与特定场景性能优化(如v-else的部分不调用render),Vue在Watcher内对dep做了特定的处理,如dep id的去重,newDepIds的刷新管理

总结

吼猴,讲完了响应式原理,我们再来看看Vue的另外一大亮点,组件化 是怎么实现的