zoukankan      html  css  js  c++  java
  • vue2.x响应式原理总结

    vue作为前端使用广泛的三大框架(react、vue、Angular)之一,vue2.x的双向数据绑定是基于Object.defineProperty实现。

     vue2.x双向数据绑定解析

    vue2.x是利用Object.defineProperty劫持对象或对象的属性的访问器,在属性值发生变化时获取属性值变化, 从而进行后续操作。

    1、Object.defineProperty在js中的描述:

    Object.defineProperty(obj, prop, descriptor) 直接在一个对象上定义一个属性,或者修改一个对象的现有 属性,并返回这个对象。

    参数:obj 要在其上定义属性的对象;prop 要定义或修改的属性的名称;descriptor 将被定义或修改的属性描述符。

    返回值: 传递给函数的对象obj

    // 定义一个对象
    const data={name:'peak',age:10}
    
    // 遍历对象 实现对对象的属性进行劫持
    Object.keys(data).forEach((key) => {
       Object.defineProperty(data, key, {
         // 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中
         enumerable: true,
         // 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除
         configurable: true, 
         get: ()=>{ // 一个给属性提供 getter 的方法
           console.info(`get ${key}-${val}`)
           return val;
         },
         set: (newVal)=>{ // 一个给属性提供 setter 的方法
           // 当属性值发生变化时我们可以进行额外操作 如调用监听器
           if(newVal === val ){ // 如果未发生变化 不做其他操作
            return;
           } 
           console.log(`触发视图更新函数${newVal}`);
         },
       });   
    });
    
    data.age=25 // 触发set方法 

    2、基于Object.defineProperty的数据劫持优势以及实现方式

    Object.defineProperty的对象以及对象属性的劫持有以下优势:

    (1)无需显示调用,如Vue2.x使用Object.defineProperty对象以及对象属性的劫持+发布订阅模式,只要数据发生变化直接通知变化 并驱动视图更新。

    (2)可在set函数中精确得知变化数据而不用逐个遍历属性获取变化值,减少性能损耗。

    实现思路:

    (1)利用Object.defineProperty重新定义一遍目标对象,完成对目标对象的劫持,在属性值变化后即触发set方法 后通知订阅者,告诉该对象的某个属性值发生了变化。

    (2)解析器Compile解析模板中的指令,收集指令所依赖的方法和数据,等待数据变化然后进行渲染。

    (3)Watcher在收到属性值发生变化后,根据解析器Compile提供的指令进行视图渲染。

    为更好的说明vue2.x的响应式原理,下面vue2.x的源码引用了Vue源码解读

    监听数据变化

    对data进行改造,所有属性设置set&get,用于在属性获取或者设置时,添加逻辑

    // Dep用于订阅者的存储和收集,将在下面实现
    import Dep from 'Dep'
    // Observer类用于给data属性添加set&get方法
    export default class Observer{
        constructor(value){
            this.value = value
            this.walk(value)
        }
        walk(value){
            Object.keys(value).forEach(key => this.convert(key, value[key]))
        }
        convert(key, val){
            defineReactive(this.value, key, val)
        }
    }
    export function defineReactive(obj, key, val){
        var dep = new Dep()
        // 给当前属性的值添加监听
        var chlidOb = observe(val)
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: ()=> {
                console.log('get value')
                // 如果Dep类存在target属性,将其添加到dep实例的subs数组中
                // target指向一个Watcher实例,每个Watcher都是一个订阅者
                // Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法
                // 此处的问题是:并不是每次Dep.target有值时都需要添加到订阅者管理员中去管理,需要对订阅者去重,不影响整体思路,不去管它
                if(Dep.target){
                    dep.addSub(Dep.target)
                }
                return val
            },
            set: (newVal) => {
                console.log('new value seted')
                if(val === newVal) return
                val = newVal
                // 对新值进行监听
                chlidOb = observe(newVal)
                // 通知所有订阅者,数值被改变了
                dep.notify()
            }
        })
    }
    export function observe(value){
        // 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
        if(!value || typeof value !== 'object'){
            return
        }
        return new Observer(value)
    }

    管理订阅者

    对订阅者进行收集、存储和通知

    export default class Dep{
        constructor(){
            this.subs = []
        }
        addSub(sub){
            this.subs.push(sub)
        }
        notify(){
            // 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
            this.subs.forEach((sub) => sub.update())
        }
    }

    订阅者

    每个订阅者都是对某条数据的订阅,订阅者维护着每一次更新之前的数据,将其和更新之后的数据进行对比,如果发生了变化,则执行相应的业务逻辑,并更新订阅者中维护的数据的值

    import Dep from 'Dep'
    export default class Watcher{
        constructor(vm, expOrFn, cb){
            this.vm = vm // 被订阅的数据一定来自于当前Vue实例
            this.cb = cb // 当数据更新时想要做的事情
            this.expOrFn = expOrFn // 被订阅的数据
            this.val = this.get() // 维护更新之前的数据
        }
        // 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
        update(){
            this.run()
        }
        run(){
            const val = this.get()
            if(val !== this.val){
                this.val = val;
                this.cb.call(this.vm)
            }
        }
        get(){
            // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
            Dep.target = this
            const val = this.vm._data[this.expOrFn]
            // 置空,用于下一个Watcher使用
            Dep.target = null
            return val;
        }
    }

    Vue

    将数据代理到Vue实例上,真实数据存储于实例的_data属性中

    import Observer, {observe} from 'Observer'
    import Watcher from 'Watcher'
    export default class Vue{
        constructor(options = {}){
            // 简化了$options的处理
            this.$options = options
            // 简化了对data的处理
            let data = this._data = this.$options.data
            // 将所有data最外层属性代理到Vue实例上
            Object.keys(data).forEach(key => this._proxy(key))
            // 监听数据
            observe(data)
        }
        // 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者
        $watch(expOrFn, cb){
            new Watcher(this, expOrFn, cb)
        }
        _proxy(key){
            Object.defineProperty(this, key, {
                configurable: true,
                enumerable: true,
                get: () => this._data[key],
                set: (val) => {
                    this._data[key] = val
                } 
            })
        }
    }

    调用这个极简版演示数据双向绑定原理的Vue

    import Vue from './Vue';
    let demo = new Vue({
        data: {
            'a': {
                'ab': {
                    'c': 'C'
                }
            },
            'b': {
                'bb': 'BB'
            },
            'c': 'C'
        }
    });
    demo.$watch('c', () => console.log('c is changed'))
    // get value
    demo.c = 'CCC'
    // new value seted
    // get value
    // c is changed
    demo.c = 'DDD'
    // new value seted
    // get value
    // c is changed
    demo.a
    // get value
    demo.a.ab = {
        'd': 'D'
    }
    // get value
    // get value
    // new value seted
    console.log(demo.a.ab)
    // get value
    // get value
    // {get d: (), set d: ()}
    demo.a.ab.d = 'DD'
    // get value
    // get value
    // new value seted
    console.log(demo.a.ab);
    // get value
    // get value
    // {get d: (), set d: ()}

    总结:

    在一些技术博客上,有人指出Object.defineProperty存在缺陷,只能监听到非数组对象的变化,而监听不到数组的变化,实际上这是错误的理解,Object.defineProperty是可以监听数组变化的,只是从性能/体验的性价比考虑,放弃了这个特性,vue设置

    7个变异数组(pushpopshiftunshiftsplicesortreverse)改用hack的方式解决数组变化的问题。

    参考资料

  • 相关阅读:
    《反恐精英》VS《使命召唤》
    CSS中expression简介实现对象批量控制
    asp无组件上传文件超过200k就提示错误的解决方法
    IE和Firefox的js兼容性整理
    国外优秀的屏幕录象专家
    陆游和唐婉
    机器智能将会在2029年达到人类的水平
    WinXP中有趣的特殊文件夹
    二行代码解决全部网页木马(含iframe/script木马)
    Javascript 获取页面高度(多种浏览器)
  • 原文地址:https://www.cnblogs.com/mzzy/p/11934266.html
Copyright © 2011-2022 走看看