[Front-end source code analysis] Data responsive principle

Reference: Vue Source Code Analysis Series Courses

Series notes:

Source code of this chapter: https://gitee.com/szluyu99/vue-source-learn/tree/master/Data_Reactive_Study

Course purpose: Thoroughly understand the principle of Vue2 data update

data responsive

MVVM pattern:

Intrusive and non-intrusive:

Object.defineProperty()

Reference documentation: Object.defineProperty() - JavaScript | MDN (mozilla.org)

Object.defineProperty()It is used for data hijacking/data proxy, using the functions endowed by the JavaScript engine to detect object property changes.

This method defines a new property directly on an object, or modifies an existing property of an object, and returns this object.

var obj = {
    
    }

Object.defineProperty(obj, 'a', {
    
    
    value: 3, // 值
    writable: false, // 只读
    enumerable: true, // 可枚举
})

console.log(obj.a) // 3
obj.a++ // 只读的,不能修改
console.log(obj.a); // 3

The method's getter/setter requires variable turnover to work:

var obj = {
    
    }
var temp // 临时变量

Object.defineProperty(obj, 'a', {
    
    
    get() {
    
    
        console.log('get a');
        return temp
    }, 
    set(newVal) {
    
    
        console.log('set a',  newVal);
        temp = newVal
    }
})

obj.a = 1
obj.a++ 
console.log(obj.a) // 2

Customize a defineReactivefunction and use closures , so you don't need to set temporary variables:

export default function defineReactive(obj, key, val) {
    
    
    if (arguments.length == 2) {
    
    
        val = obj[key]
    }
    Object.defineProperty(obj, key, {
    
    
        enumerable: true, // 可枚举
        configurable: true, // 可配置
        get() {
    
    
            console.log('get', key);
            return val
        },
        set(newVal) {
    
    
            console.log('set', key, newVal);
            if (val === newVal) return
            val = newVal
        }
    })
}
let obj = {
    
    }

// 实现了数据响应式
defineReactive(obj, 'a', 1)
console.log(obj.a); // 1
obj.a = 10
console.log(obj.a); // 10

Recursively detect all properties of an object

For the following objects, use the defineReactiveabove obj.a.m.nproperties that cannot be monitored (only one layer can be monitored, and multiple layers cannot be monitored)

let obj = {
    
    
    a: {
    
    
        m: {
    
    
            n: 1
        }
    },
    b: 1
}

The desired effect: observemake all properties of obj responsive

observe(obj)
obj.b++ // 响应式
obj.a.m.n++ // 响应式

Program flow chart: (The effect of recursion is realized through the calls between functions at all levels)

observe.js

/**
 * 将 obj 所有属性变为响应式
 */
export default function observe(obj) {
    
    
    if (!obj || typeof obj !== 'object') return
    var ob;
	// 判断是否已经是响应式
    if (typeof obj.__ob__ != 'undefined') {
    
    
        ob = obj.__ob__
    } else {
    
    
        ob = new Observer(obj)
    }
    return ob
}

Observer.js

/**
 * 将一个正常的 object 转换成每个层级的属性都是响应式的 object
 */
export default class Observer {
    
    
    constructor(obj) {
    
    
        console.log('Observer constructor', obj);
        // 构造函数中的 this 不是类本身,而是表示实例
        def(obj, '__ob__', this, false)
        // 将 object 中的属性转换成响应式的属性
        this.walk(obj)
    }
    // 遍历 object 的属性,将其转换成响应式的属性
    walk(obj) {
    
    
        console.log('walk', obj);
        for (let k in obj) {
    
    
            defineReactive(obj, k)
        }
    }
}

/**
 * 对 Object.defineProperty 的封装
 */
export const def = function (obj, key, value, enumerable) {
    
    
    Object.defineProperty(obj, key, {
    
    
        value,
        enumerable,
        writable: true,
        configurable: true
    })
}

defineReactive.js

/**
 * 将数据变为响应式
 */
export default function defineReactive(obj, key, val) {
    
    
    console.log('defineReactive', key);
    if (arguments.length == 2) {
    
    
        val = obj[key]
    }

    // 子元素要进行 observe,至此形成递归
    // 这个递归是多个函数、类循环调用
    let childOb = observe(val)

    Object.defineProperty(obj, key, {
    
    
        enumerable: true, // 可枚举
        configurable: true, // 可配置
        get() {
    
    
            // console.log('get', key);
            return val
        },
        set(newVal) {
    
    
            console.log('set', key, newVal);
            if (val === newVal) return
            val = newVal
            // 当设置了新值,这个新值也要被 observe
            childOb = observe(newVal)
        }
    })
}

Effect:

let obj = {
    
    
    a: {
    
    
        m: {
    
    
            n: 1
        }
    },
    b: 1
}

observe(obj)
obj.b++ // 响应式
obj.a.m.n++ // 响应式

The responsive principle of arrays

For array objects, the above implementation cannot monitor its push, , popand other element modification methods:

let obj = {
    
    
	c: [1, 2, 3, 4]
}

Override 7 methods: push, pop, shift, unshift, splice, sort,reverse

array.js

// 数组的原型
const arrayPrototype = Array.prototype

// 以 Array.prototype 为原型创建 arrayMethods 对象,并暴露
export const arrayMethods = Object.create(arrayPrototype)

// 要被改写的 7 个数组方法
const methodsNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]

// 数组方法实际是自己写的,自己写的再调用真实的数组方法,中间可以拦截数据
methodsNeedChange.forEach(methodName => {
    
    
    // 备份原来的方法
    const originMethod = arrayPrototype[methodName]
    // 给原型定义新的方法
    def(arrayMethods, methodName, function () {
    
    
        console.log('arrayMethods', methodName);

        // 执行原来的函数
        const result = originMethod.apply(this, arguments)
        // arguments 是伪数组对象,转成数组对象
        const args = [...arguments]

        // 把数组身上的 __ob__ 取出来
        const ob = this.__ob__

        // 有三种方法 push / unshift / splice 可以插入新项
        // 要将插入的新项也变为 observe 的
        let inserted = []

        switch (methodName) {
    
    
            case 'push':
            case 'unshift':
                inserted = args
                break;
            case 'splice':
                // splice(下标, 数量, 插入的新项)
                inserted = args.slice(2)
                break;
        }

        // 判断有没有要插入的新项,让新项也变为响应的
        if (inserted) ob.observeArray(inserted)

        return result
    }, false)
})

Modify the code to construct Observer Observer.jsin :

/**
 * 将一个正常的 object 转换成每个层级的属性都是响应式的 object
 */
export default class Observer {
    
    
    constructor(obj) {
    
    
        console.log('Observer constructor', obj);
        // 构造函数中的 this 不是类本身,而是表示实例
        def(obj, '__ob__', this, false)
        // 将 object 中的属性转换成响应式的属性
        if (Array.isArray(obj)) {
    
    
            // 将数组的原型指向 arrayMethods
            Object.setPrototypeOf(obj, arrayMethods)
            // 让数组变的 observe
            this.observeArray(obj)
        } else {
    
    
            this.walk(obj)
        }
    }
    // 遍历 object 的属性,将其转换成响应式的属性
    walk(obj) {
    
    
        console.log('walk');
        for (let k in obj) {
    
    
            defineReactive(obj, k)
        }
    }
    // 数组的特殊遍历
    observeArray(arr) {
    
    
        for (let i = 0, l = arr.length; i < l; i++) {
    
    
            // 逐项进行 observe
            observe(arr[i])
        }
    }
}

Effect:

let obj = {
    
    
    c: [1, 2, 3, 4]
}

observe(obj)
obj.c.push(55, 66, 77)
obj.c.splice(1, 1, [1, 2])
console.log(obj.c);

Dependency collection

What is dependency? Where data is needed, it is called dependency

  • In Vue 1.x, dependencies are fine-grained , and the DOM used for data is dependent on
  • In Vue 2.x, dependencies are medium-grained , and components are dependencies
  • Collect dependencies in getters and trigger dependencies in setters

Dep class and Watcher class:

  • The Dep class encapsulates the code of dependency collection and is dedicated to managing dependencies. Each Observer instance has an instance of Dep in its members
  • Watcher is an intermediary, when the data changes, it will be transferred through Watcher to notify the component

  • Dependency is Watcher. Only the getter triggered by Watcher will collect dependencies. Which Watcher triggers the getter will collect which Watcher to Dep
  • Dep uses the publish-subscribe mode. When the data changes, it will cycle the dependency list and notify all Watchers once.
  • The ingenuity of the code implementation: Watcher sets itself to a specified location globally, and then reads the data. Because the data is read, the getter of this data will be triggered. In the getter, you can get the Watcher that is currently reading data, and collect this Watcher into the Dep.

Reference article: Vue's in-depth responsive principle

Effect:

let obj = {
    
    
    a: 1,
    b: {
    
    
        m: {
    
    
            n: 1
        }
    },
    c: [1, 2, 3, 4]
}

observe(obj)

// 监控依赖
new Watcher(obj, 'b.m.n', val => {
    
    
    console.log('Watcher 在监控 b.m.n', val)
}) 

obj.b.m.n++ 
console.log(obj)

Dep.js

/**
 * 全局唯一的 依赖收集器
 */
export default class Dep {
    
    
    constructor() {
    
    
        // console.log('Dep constructor'); 
        this.id = uid++

        // 用数组存储自己的订阅者,这个数组中存放 Watcher 实例
        this.subs = [] // subscribers
    }
    // 添加订阅
    addSub(sub) {
    
    
        this.subs.push(sub)
    }
    // 添加依赖
    depend() {
    
    
        // Dep.target 就是自己指定的全局的位置(window.targte 也可以,全局唯一即可)
        if (Dep.target) {
    
    
            this.addSub(Dep.target)
        }
    }
    // 通知更新
    notify() {
    
    
        console.log('Dep notify'); 
        // 浅克隆一份
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
    
    
            subs[i].update()
        }
    }
}

Watcher.js

var uid = 0

export default class Watcher {
    
    
    // 监听 target 对象的 expression 属性,执行 callback 回调
    constructor(target, expression, callback) {
    
    
        // console.log('Watcher constructor');
        this.id = uid++
        this.target = target
        this.getter = parsePath(expression) // 解析 expression 为一个函数
        this.callback = callback
        this.value = this.get()
    }
    update() {
    
    
        // console.log('Watcher update');
        this.run()
    }
    get() {
    
    
        // 进入依赖收集阶段,让全局 Dep.tartget 设置为 Watcher 本身
        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)
        }
    }
}

// 返回一个可以解析 "a.b.c" 格式的函数
// let fn = parsePath('a.b.c')
// fn({ a: { b: { c: 1 } } })
function parsePath(str) {
    
    
    var segments = str.split('.');
    return obj => {
    
    
        for (let i = 0; i < segments.length; i++) {
    
    
            if (!obj) return
            obj = obj[segments[i]];
        }
        return obj
    }
}

The rest of the file code is omitted...

Complete source code reference: https://gitee.com/szluyu99/vue-source-learn/tree/master/Data_Reactive_Study

Guess you like

Origin blog.csdn.net/weixin_43734095/article/details/125488301