最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 一步一步学习Vue2 响应式原理

    正文概述 掘金(酔墨)   2021-01-31   749

    1.前言

    vue的核心思想便是数据驱动与组件化,我们在使用vue来开发日常工作项目的时候可以大大的减少来我们对原生dom的操作,使得写起代码也是得心应手。那么在vue中是如何实现这样的响应式系统的呢?今天来一探究竟。

    2. 响应式原理


    在vue2 中响应式系统是通过 发布订阅模式来实现的。 举个例子?,从前有一个 小s (sourcedata)梦想成为一个受大家欢迎的人,于是他就去问他的朋友 小 O(observe)该如何才能成为一个受大家欢迎的人呢?于是小O送了一部最新款iphone 小D(dep)给小s ,并告诉小s 说只要你肯乐于助人,终有一天可以成为一个受大家欢迎的人。于是小s 拿着最新款iphone 高高兴兴的回家了。花开两朵、各表一枝。小a最近手头紧, 听说小s是远近闻名的好人,于是便想这找小s借点钱来应应急,可是自己拉不下脸去找小s借钱,于是便委托小w(watcher)去帮忙,于是小w帮小a 从小s 处借了(get)1000元。热心的小s在小w临走时还不忘将小w的电话☎️记在(depend)自己的 iPhone(dep)上,并且对小w说等这个月我发工资了再通知你来拿钱,转眼间到了发工资的日子了,小s 工资刚领到手还没捂热呢,便想起来小w借钱的事,于是用 自己的 iphone (dep)通知(notify)小w自己今天发工资了 ,可以来借钱了。于是 小w便再次(update)来帮小a找小s借钱。你们猜最终小s有成为受大家欢迎的人么(?)

    上面在小s成为一个受大家欢迎的人的故事中有几个重要的角色,小O , 小D , 小W。他们(它们)三个是小s成长道路上必不可少的三位;他们的扮演的角色大致如下:

    • 小O:妥妥的导师,负责指明 小s成长道路的方向(毕竟如果方向不对,兄der你懂的 ), 与此同时 也负责给小s提供工具(iphone)
    • 小D: 妥妥的工具,负责记录小s想帮助的人的联系方式,在必要的时候可以到联系那些需要帮助的人。
    • 小W:妥妥的中间商(还好他不赚差价?),负责将自己的联系方式留给小s,还负责在小s通知自己的时候去联系那些通过自己去找小s寻求帮助的人

    所以,接下来要做的就是将上面的故事用代码描绘出来了吧?

    那么首先第一个出场的角色就是 小s ,所以我们给他来个简单定义

    const data = {
          count: 1
    }
    

    接下来就是第二个,导师的角色了

    Observe 类

    import Dep from './Dep'
    interface IObserve {
        [key: string]: any
    }   
    
    export default class Observe {
        value: IObserve | Array<any>;
        constructor(value: IObserve | Array<any>) {
            this.value = value
          		// 对象处理
            if (isPlainObject(this.value)) {
                this.walk()
            } else {
                //  劫持数组
            }
        }
        walk() {
          // 人生导师,开始指路(手动滑稽+?)
            Object.keys(this.value).forEach(key => {
                defineReactive(this.value, key)
            })
        }
    }
    
    export function defineReactive(target: IObserve, key: string) {
        let val = target[key];
      	// 财大气粗的小O 直接送一部iphone 小D 给小s ,当然,如果小s 内有很多个
        //key时,那么我们的小o就会送给小s和key数量相同的iphone,毕竟人傻钱多(?️)
        const dep = new Dep()
        Object.defineProperty(target, key, {
            get() {
              // 当有人来借钱了,就给他记在iphone的通讯录上
                dep.depend()
                return val
            },
            set(newVal) {
                if (val != newVal) {           
                    val = newVal
                  	// 发工资了,掏出我的iphone,逐个联系,速来领钱 ?
                    dep.notify()
                }
            }
        })
    }
    

    首先 在Observe 类中 会循环遍历目标对象的键名,然后通过这些键名来调用 Object.defineProperty Api来劫持目标对象上的getter、setter。与此同时这也是 Vue2 中响应式的缺点,因为这个Api调用的前提是要知道目标对象的键名,所以在对于 vue初始化之后再向 data对象中添加键值对,它们是不具备响应式。虽然vue2中增加了$set这样的api来弥补这个Api的不足,但这仍然增加了使用成本与学习成本。所以vue3采用的Proxy代理的方式来实现数据的劫持。

    接着第三个角色,便是 小D 了,虽然它只是一个工具?

    Dep 类

    
    import Watcher from './Watcher'
    export default class Dep {
        static target: Watcher | null | undefined = null;
        subscribes: Set<Watcher>;
        constructor() {
          // 初始化依赖数组,用Set可以自动去重
            this.subscribes = new Set()
        }
        depend() {
            //  收集依赖于当前数据的更新函数
          	// 当有人来借钱,就将它记在这个小本本上
            if (Dep.target) {
                this.subscribes.add(Dep.target)
            }
        }
        notify() {
            //  通知依赖更新数据,重新计算值
          // 什么?! 小s打电话来说发工资了,小a、小b、小c、小d还不快去领钱? ,
          //领完钱他们的资产该从新计算了
            this.subscribes.forEach(watcher => watcher.updated())
        }
    }
    
    const targetStack: Array<Watcher> = [];
    export function pushTarget(target: Watcher) {
        targetStack.push(target)
      	// 便于找到当前正在执行的更新函数
        Dep.target = target
    }
    export function popTarget() {
        targetStack.pop()
        Dep.target = targetStack[targetStack.length - 1]
    }
    

    Dep 类作为收集 watcher 依赖的类而存在,它具有以下特点:

    • 它负责在访问被劫持的数据的时候将当前正在执行的 更新函数Watcher(即那些依赖于当前数据的更新函数)收集起来;
      • 这些更新函数在进入JavaScript 函数执行栈之前会被压入vue创建的一个类似于执行栈的更新函数的数组中,同时也会保存在Dep的一个静态属性target上,因此可以通过访问全局变量 Dep.target来获取到此时正在执行的更新函数。
    • 在数据被修改的时候Dep调用notify方法去通知被它收集起来的watcher去执行update方法更新数据。
    
    class Dep {
      // 在更新函数执行时,会被保存在 Dep的target属性上,可以通过 Dep.target 
      //来访问到此时正在执行的更新函数(⚠️注意这个Dep.target 其本质就是一个全局变
      //量而已,不要多想?,它仅仅是挂在Dep这个类上)
        static target: null | undefined | Watcher = null
        subs: Set<Watcher>;
        constructor() {
          // 使用set 创建,避免收集重复的watcher
            this.subs = new Set()
        }
        depend() {
          // 如果此时的更新函数有值,那么便可以将它收集起来
            if (Dep.target) {
                this.subs.add(Dep.target)
            }
        }
        notify() {
          	// 当前dep所在的数据更新了,通知依赖当前这个数据的 watcher去执行更新数据
            this.subs.forEach(watcher => watcher.update())
        }
    }
    
    // definedReactive方法也要改写以下,要加上依赖收集的逻辑
    
    function defineReactive(target: IReactive, key: string) {
        let value = target[key]
        // 为每一个 key 创建一个 dep的实例,这个dep中 会存放着依赖于这个key值的更新函数
        const dep = new Dep()
        Reflect.defineProperty(target, key, {
            get() {
              // 收集依赖 ,如果 有更新函数访问了 key所对应的值,那么就代表这它
              //依赖于当前的key,所以要将它收集起来,当key改变之后,通过这个更
              //新函数,让他重新计算值
                dep.depend()
                return value
            },
            set(newVal) {
                if (value != newVal) {
                    value = newVal
                  // 当前 key 对应的值发生了改变 , 通知依赖于这个key 值的
                  //Watcher 去进行更新重新计算值
                    dep.notify()
                }
            }
        })
    }
    
    const targetStack: Array<Watcher | undefined> = []
    function pushTarget(target: Watcher | undefined) {
        targetStack.push(target);
        Dep.target = target
    }
    
    function popTarget() {
        targetStack.pop()
        Dep.target = targetStack[targetStack.length - 1]
    }
    
    


    接着便是最后一个角色的出场了,不赚差价的中间商来也!?

    Watcher 类

    
    import Dep, { pushTarget, popTarget } from './Dep'
    type updateFC = () => any
    export default class Watcher {
        updateFn: updateFC;  
        value: any;// update 更新函数的结果值
        constructor(updateFn: updateFC) {
            // 保存更新函数
          	// 小a上次说让我从小s借多少来着?还是给它记在我的小本本上好嘞,方便
            //我下次去找小s帮小a借钱
            this.updateFn = updateFn
            this.get()
        }
        get() {
          	// 把我的联系方式写在全局,方便小s记录我的联系方式
            pushTarget(this)
          	// 借钱、再次借钱、又去借钱。。。
            const value = this.updateFn()
            popTarget()
            return value
        }
        updated() {
            this.get()
        }
    }
    

    Watcher 中最重要的作用就是要存储执行数据更新的函数,以及在适当的时机再去执行数据更新函数。

    图解

    上面的故事有些生硬?,但确实是我所理解的vue的响应式(如有理解错误的地方,请务必指出,感谢大家。?),下面再给大家表演个看图说话,梳理一下一个响应式数据创建以及工作的大致流程:
    一步一步学习Vue2 响应式原理

    总结

    如今vue3也出了好久了,前端这个行业发展的真是迅速,一天不学就会落后,希望这个时候才开始看vue2的源码不会太迟吧?,一起加油呀⛽️!


    起源地下载网 » 一步一步学习Vue2 响应式原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元