Vue源码之响应式数据原理

Vue源码之响应式数据原理

数据驱使着页面的变化,手动实现Vue底层的响应式数据原理

1. 侵入式与非侵入式

在Vue中我们修改值不需要API,例如 this.a++ 这就是非侵入式

而在React中,必须要使用React给我们提供的API 例如

this.setState({
    a: this.state.a + 1
})

这就是侵入式

2. Object.defineProperty()方法

const conf = {
    name: "小明",
    age: 18,
}
Object.defineProperty(conf, "sex", {
    value: "男",
    //是否可写
    writable: false,
    //是否可以被枚举
    enumerable: false,
    //是否可以被改变和删除
    configurable:false,
})
Object.defineProperty(conf,"sex",{
    //当有人访问sex值的时候被访问
    get(){
        return conf.sex
    },
    //当有人修改sex的值的时候被调用
    set(newValue){
            
    }
})

3.defineReactive函数

Object.defineProperty()方法有一个很大的问题,就是当我们用get和set时必须需要一个中转变量

这时我们就可以定义一个defineReactive函数,来构造一个闭包环境

let obj = {}

function defineProperty(data, key, value) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            return value
        },
        set(v) {
            if (v === value) return
            value = v
        }
    })
}

defineProperty(obj, "a", 10)
console.log(obj.a)
obj.a = 11
console.log(obj.a)

接下里手动实现Vue响应式数据原理

4.index.js入口文件

4.1配置webpack环境实现模块式开发

我们使用webpack的版本如下

"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.3"

webpack.config.js文件

const path = require("path")

module.exports = {
    mode: "development",
    entry: './src/index.js',
    output: {
        filename: "bundle.js"
    },
    devServer: {
        contentBase: path.join(__dirname, "www"),
        compress: false,
        port: 8080,
        publicPath: "/xuni/"
    }
}

4.2 index.js文件

import {observe} from "./observe";

let obj = {
    a: {
        m: {
            n: 5
        }
    },
    b: 18,
    g: [22, 33, 44, 55, [11, 22, 33]]
}

observe(obj)

我们要把obj这个对象里的所有数据都变成响应式的

5. observe函数

//Create an observe function to give an aid to the Observer class
import Observer from "./Observer";

export const observe = function (value) {
    //This function serves only objects
    if (typeof value !== 'object') return
    let ob
    if (typeof value.__ob__ !== 'undefined') {
        ob = value.__ob__
    } else {
        ob = new Observer(value)
    }
    return ob
}

这个函数的主要功能是给数据添加一个Observer类,且这个函数只为对象服务

6. Observer类

import {def} from './utils'
import defineReactive from "./defineReactive";
import {arrayMethods} from "./array";
import {observe} from "./observe";

//The purpose of the Observer class
//Converting a normal Object to Objects that are responsive (can be detected) at each level
export default class Observer {
    constructor(value) {
        def(value, "__ob__", this, false)
        if (Array.isArray(value)) {
            //Force a change in the prototype chain
            Object.setPrototypeOf(value, arrayMethods)
            this.observeArray(value)
        }else {
            this.walk(value)
        }
    }

    walk(value) {
        for (let k in value) {
            defineReactive(value, k)
        }
    }

    observeArray(arr) {
        for (let i = 0, l = arr.length; i < l; i++) {
            observe(arr[i])
        }
    }
}

其实响应式数据原理的底层就是递归,但不是普通的递归,是几个js文件相互调用就形成了递归

6.1首先看对象的响应式

在类的构造器里将对象传入进去,如果是非数组的数据变为响应式的话,调用自身的walk方法,然后调用defineReactive函数

6.2 而数组的响应式比较复杂

在学习Vue是我们知道,如果想让Vue知道数组的侦测的话,只能有7种方法才可以被侦测的到,分别是

'push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"

Vue底层的方法是重写这几个方法,强制改变他们的原型链,然后再调用数组自己的方法实现响应式

7. defineReactive函数

import {observe} from "./observe";

export default function defineReactive(data, key, value) {
    if (arguments.length === 2) {
        value = data[key]
    }
    let childOb = observe(value)
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            return value
        },
        set(newValue) {
            if (value === newValue) return
            value = newValue
            childOb = observe(newValue)
        }
    })
}

这个文件的作用就是别别人调用时,依次为传入的值匹配 getter和setter实现响应式

在此时数组非常的特殊,如果有人修改了数组的值就会调用setter方法,在setter我们再重新调用observe函数,重新将数组的数据转为响应式,实现递归

8. array.js

这个文件集中处理数组的问题

import {def} from "./utils";

//To make the array responsive, we will rewrite the seven methods of the array
// ['push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"]
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = ['push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"]
methodsNeedChange.forEach(methodName => {
    let original = arrayPrototype[methodName]
    def(arrayMethods, methodName, function () {
        const args = [...arguments]
        const ob = this.__ob__
        let inserted = []

        switch (methodName) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
        }
        if (inserted) {
            ob.observeArray(inserted)
        }

        return original.apply(this, arguments)
    }, false)
})

首先将数组的原型关系复制一遍,创造一个新的对象,将复制的原型转化为arrayMethods对象的原型,再将上面的7个方法重写,我们只是添加一些功能,我们要将原始的功能original返回出去,这样就不会干扰数组方法的正常功能

我们导入def模块,就是Object.defineProperty()的作用,将新写的这7个方法放入arrayMethods这个对象中,然后对外暴露

但其中这3个方法比较特殊,分别是 push unshift spilce,他们的功能其实就是新加数据,所以当调用时我们就调用自身身上的observeArray方法,将新入的数据变为响应式,其实就是这些具有功能的类和函数,相互调用,实现递归

9. def函数

export const def = function (obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable: true,
        configurable: true
    })
}

其功能就是给 array.js其辅助作用,就是往arrayMethods身上写入你重写的数组的方法

10.小结:

Vue源码的中将数据变为响应式,也就是我上面写的,但Vue写的一定更加完善,他的方法就是这些具有功能的类和函数,相互调用,实现循环递归

11. 依赖

11.1 什么是依赖:

  • 就是需要用到数据的地方,就是依赖

11.2 Vue的思路:

  • 在getter时收集依赖,在setter时触发依赖

12. Dep类和Watcher类

  • 依赖就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中
  • Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍
  • Vue设计的巧妙之处就是把Watcher设置到全局的一个指定位置,然后读取数据,因为读取了数据,就会触发数据的getter方法,在getter中就能得到当前数据的Watcher,并把这个Watcher收集到Dep中

13. Dep类

let uid = 0

export default class Dep {
    constructor() {
        this.id = uid++
        //The array holds its own subscribers
        //It's also an instance of Watcher
        this.subs = []
    }

    //Add a subscription
    addSub(sub) {
        this.subs.push(sub)
    }

    //Add the dependent
    depend() {
        if (Dep.target) {
            this.addSub(Dep.target)
        }
    }

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

Dep类中存放的就是Watcher类实例

14.混入Dep类的defineReactive函数

import {observe} from "./observe";
import Dep from "./Dep";

export default function defineReactive(data, key, value) {
    const dep = new Dep()
    if (arguments.length === 2) {
        value = data[key]
    }
    let childOb = observe(value)
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                }
            }
            return value
        },
        set(newValue) {
            if (value === newValue) return
            value = newValue
            childOb = observe(newValue)
            dep.notify()
        }
    })
}

在getter时收集依赖,再接管数据,将数据变为响应式时,同时实例Dep类,当有人调用getter类时,就会收集依赖

15.混入Dep类的 Observer类

import {def} from './utils'
import defineReactive from "./defineReactive";
import {arrayMethods} from "./array";
import {observe} from "./observe";
import Dep from "./Dep";

//The purpose of the Observer class
//Converting a normal Object to Objects that are responsive (can be detected) at each level
export default class Observer {
    constructor(value) {
        //Every instance of an Observer has a DEP on it
        this.dep = new Dep()
        def(value, "__ob__", this, false)
        if (Array.isArray(value)) {
            //Force a change in the prototype chain
            Object.setPrototypeOf(value, arrayMethods)
            this.observeArray(value)
        } else {
            this.walk(value)
        }
    }

    walk(value) {
        for (let k in value) {
            defineReactive(value, k)
        }
    }

    observeArray(arr) {
        for (let i = 0, l = arr.length; i < l; i++) {
            observe(arr[i])
        }
    }
}

在调用Observer类时同时也也实例化Dep类

16.混入Dep类的array.js文件

在响应化数组的时候也要实例Dep类

import {def} from "./utils";

//To make the array responsive, we will rewrite the seven methods of the array
// ['push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"]
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = ['push', 'pop', 'shift', "unshift", "splice", "sort", "reverse"]
methodsNeedChange.forEach(methodName => {
    let original = arrayPrototype[methodName]
    def(arrayMethods, methodName, function () {
        const args = [...arguments]
        const ob = this.__ob__
        let inserted = []

        switch (methodName) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
        }
        if (inserted) {
            ob.observeArray(inserted)
        }
        ob.dep.notify()
        return original.apply(this, arguments)
    }, false)
})

注:ob对象里有dep实例,因为在数组调用混入Dep类的 Observer类时就以添加,所以直接使用就可

17. Weather类

import Dep from "./Dep";

let uid = 0

export default class Watcher {
    constructor(target, expression, callback) {
        this.id = uid++
        this.target = target
        this.getter = parsePath(expression)
        this.callback = callback
        this.value = this.get()
    }

    update() {
        this.run()
    }

    get() {
        Dep.target = this
        const obj = this.target
        let value
        try {
            value = this.getter(obj)
        } finally {
            Dep.target = null
        }
        return value
    }

    run() {
        this.getAndInvoke(this.callback)
    }

    getAndInvoke(cb) {
        const value = this.get()
        if (value !== this.value || typeof value === "object") {
            const oldValue = this.value
            this.value = value
            cb.call(this.target, value, oldValue)
        }
    }
}

function parsePath(str) {
    let segments = str.split(".")
    return (obj) => {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return
            obj = obj[segments[i]]
        }
        return obj
    }
}

当数据发生更新时,Dep类的update()函数触发Weather类的run方法,此时Weather类被调用,没有人引用Weather类,它别Vue的指令例如v-model等调用,Weather类得到最新的结果通知渲染,在进行后面虚拟DOM和diff算法,最小量更新,最后完成重新解析模板

这也就是响应式数据的原理

18. 注:

代码含webpack构建工具已传至github仓库

https://github.com/Bald-heads/dataResponsiveness.git
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值