Vue原理从0到1实现变化侦测

751 阅读6分钟

为什么要理解源码?

随着前端技术的不断发展,人们越来越追求更简便、更快捷的开发方式,目前业内流行的三大主流框架Angular、React、Vue代替了繁琐的dom的操作,使用数据驱动视图的方式,使我们前端跨越一大步的同时,也让人越来越关注外功、插件与最佳实践。

但是其实内功才是一切的根本,框架总有被淘汰的时候,但是JS永不过时,并且万物殊途同归,了解了Vue的实现原理后,对学习React以及今后的发展会有很大的帮助

什么是Vue?

“the fate of destruction is also the jpy of rebirth”(毁灭的命运也是重生的喜悦)

这是Vue诞生之初作者发布的格言,标志着Vue的诞生.在vue诞生之初,没有人会觉着这个由中国人写的框架,会在如今超过脸书的React和谷歌的Angular,成为使用人数最多,最主流的Web框架,下到小程序、网页,上到APP都有它的影子。

Vue.js(/vjuː/,简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用(SPA)的Web应用框架。很多年前,那个前端还被叫做切图仔的年代,页面的主要功能都是由后端掌控的,通过javaWeb(后端服务),模板引擎(jsp等)等技术,显著特点(多页面)

  • 多页面应用:每个页面之间相互独立,有自己的独有资源(比如后端接管路由,实现多页面跳转的形式)
    
  • 单页面应用(SPA):只有一个页面,看似多页面其实是通过DOM元素的销毁与创建形成的,前端正式接管路由!
    

Vue采用MVVM(数据驱动视图)的模式,去充当MVVM中的VM层,在数据层与视图层间做双向响应,使前端开发可以通过只改变数据,进而改变视图,其中用到的原理便是JS新提供原生的API——Object.defineProperty去实现双向数据绑定(注意:React的双向数据绑架不是这种形式)

Object.defineProperty(双向数据绑定)

好了各位,说了这么多,终于可以开始今天的主题,defineProperty的原理

我们正式封装一个这样的函数去劫持数据,这个函数可以对对象的属性进行劫持,当使用这个这个属性值的时候,进入defineProperty的get方法里并得到return回来的返回值,改变这个属性值的时候,进入到了set方法里

        /**
         * 封装一个简单的object.definePropoty实现数据的劫持
         * data:对象
         * key:属性
         * value:值
         */
        function defineReactive(data, key, value) {
            Object.defineProperty(data, key, {
                enumerable: true, //可枚举
                configurable: true, //可配置
                get() {
                    console.log("进入到了get方法里");
                    return value
                },
                set(newVal) {
                    console.log("进入到了set方法里");
                    if (value === newVal) return
                    value = newVal
                }
            })
        }
       
        // 定义一个普通的对象
        const obj = {
            val: "1"
        }
        //进行劫持数据
        defineReactive(obj,"val",obj.val)
        //使用了属性值,进入get方法里,并且输出了return回来的返回值
        console.log(obj.val)
        //修改了属性值,进入set方法里
        obj.val=2

在vue中,我们知道,一个数据的更新会触发组件的更新,那么是怎么做的呢,简单,既然已经知道了有object.defineProporty这个新颖的API,并且数据的改变会进入到set方法里,那么我们可以大胆的猜想一下,在set方法里,触发了组件更新的方法,接下来,让我们继续改造这个函数

Vue中的依赖收集

了解完vue的响应式原理,就让我们来接触一下Vue是怎么利用它来工作的叭。

首先,我们假设一下这样的场景,我们知道数据的更新会触发set方法中的组件更新,那么是所有的data数据都会触发组件更新嘛?显然不是的,因为这样会很损失性能,我们希望只有改变被使用了并且渲染在页面上的data才会触发组件的更新,这就是依赖收集的由来。

Vue进行依赖收集的原理参照了发布订阅模式:

在软件架构中,发布/订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者),而是通过消息通道(消息中间间)广播出去,让订阅改消息主题的订阅者消费到。

参照这个模式,我们来改写我们封装的代码,

  1. 首先设置一个消息中间件数组Dep
  2. 再将所有被使用过的数据(get的时候)收集进入了数组里
  3. 再数据被更改(set的时候)时,触发组件的updata方法,这里用log数据代替。
        /**
         * 利用封装的劫持函数,进行最简单的依赖收集
         * 依赖收集的实质是收集组件,这里用最简单的函数进行代替
         * 依赖收集靠的是 发布订阅模式
         * dep实际上就是一个发布订阅的中介
         */
        let dep = [] //发布订阅数组——消息中间件
        function defineReactive(data, key, value) {
            Object.defineProperty(data, key, {
                enumerable: true, //可枚举
                configurable: true, //可配置
                get() {
                    // 收集依赖
                    dep.push("收集依赖")
                    console.log(1);
                    return value
                },
                set(newVal) {
                    console.log(" 进入到了set方法里");
                    if (value === newVal) return
                    value = newVal
                    // 触发依赖,实际上就是遍历所有的dep数组
                    for (let i = 0; i < dep.length; i++) {
                        console.log(dep[i]); //输出整个dep数组,实际中是触发dep[i].updata()
                    }
                }
            })
        }
        // 定义一个普通的对象
        const obj = {
            val1: "1",
            val2: "2"
        }
        //模拟遍历,对obj属性进行数据劫持
        defineReactive(obj, "val1", obj.val1)
        defineReactive(obj, "val2", obj.val2)


        // 收集依赖
        console.log(obj);
        console.log("收集依赖:"+obj.val1);
        console.log("收集依赖:"+obj.val2);
        obj.val2=3//触发依赖

上面只是对依赖收集的一个模拟,那么问题来了,很多会疑惑,在实际中,vue依赖收集,到底收集了什么呢?收集的是watcher,watcher其实就是组件,那么watcher到底是如何被收集进dep数组中的呢,接下来我们继续修改我们的代码

封装watcher和Dep

在实际开发中,我们dep需要收集的类型可能更为复杂,我们需要定义一个dep类来专门管理这个数据:

        /**
         * dep类,专门用来管理依赖
         * 假设依赖是一个函数,存在window.target上
         * window.target其实全局的内存空间,在这里代表watcher
         */
        class Dep {
            //构造函数
            constructor() {
                this.subs = [] //为原型对象上的属性
            }
            // 添加数组
            addSub(sub) {
                this.subs.push(sub)
            }
            //移除数组
            removeSub(sub) {
                remove(this.subs, sub)
            }
            //收集依赖
            depend() {
                //判断是否为一个watcher类型,如果是就收集watcher
                if (window.target) {
                    console.log(window.target);
                    this.addSub(window.target)
                }
            }
            //通知组件
            notify() {
                const subs = this.subs.slice()
                console.log(subs);
                for (let i = 0, l = subs.length; i < l; i++) {
                    subs[i].updata()
                }
            }
        }
        //工具函数
        function remove(arr, item) {
            if (arr.length) {
                const index = arr.indexOf(item)
                if (index > -1) return arr.splice(index, 1)
            }
        }
        

接下来,我们继续封装,发布订阅者模式中,最终的观察者——watcher,我们希望watcher在组件被挂载的时候被创建,并且能自动被进入dep数组中,当数据被修改时,触发组件的更新,watcher就是组件!!!,其实在实际中,只有使用watcher,才会被收集进中介里,那么我们继续封装一个watcher类,来实现上面的功能:

        /**
         * watcher类
         * watcher的本质就是一个实例对象
         * 会触发get方法主动将自己添加到dep中去
         * 关键问题:watcher在哪里被创造的呢?????????
         */
        class watcher {
            constructor(vm, expOrFn, cb) {
                this.vm = vm
                //判断expOrFn,也就是属性的合理性,返回一个能读取自己值得函数
                this.getter = parsePath(expOrFn)
                this.cb = cb
                this.value = this.get()
            }
            get() {
                console.log('进入watcher的到get方法里了');
                window.target = this //把watcher对象存进内存中
                let value = this.getter.call(this.vm, this.vm) //在读一下自己的值,使其加入依赖
                window.target = undefined //清空内存
                return value
            }
            //updata方法也就是组件更新的方法
            updata() {
                console.log('进入watcher的到updata方法里了');
                const oldValue = this.value
                this.value = this.get()
                this.cb.call(this.vm, this.value, oldValue)
            }
        }
        // 返回一个能解析对象最终值得函数
        const bailRE = /[^\w.$]/
        function parsePath(path) {
            if (bailRE.test(path)) return
            const segments = path.split('.')
            return function (obj) {
                for (let i = 0; i < segments.length; i++) {
                    if (!obj) return
                    obj = obj[segments[i]]
                }
                console.log(obj);
                return obj
            }
        }
        

这样我们就实现好了watcher类,当被watcher实例被new出来的时候,就会通过this.getter方法读取一下自己的值,这样就会触发defineProporty中的get方法,从而将自己添加到Dep中去

改装一下我们的劫持函数,使其通过封装好的dep工具类去收集和管理依赖

       /**
         * 改装劫持函数
         */
         function defineReactive(data, key, value) {
            let dep = new Dep() //创造一个Dep实例
            Object.defineProperty(data, key, {
                enumerable: true, //可枚举
                configurable: true, //可配置
                get() {
                    console.log(" 进入到了get方法里");
                    dep.depend() //收集依赖
                    return value
                },
                set(newVal) {
                    console.log(" 进入到了set方法里");
                    if (value === newVal) return
                    value = newVal
                    // 触发依赖
                    dep.notify()
                }
            })
        }

注意:Dep.depend()中做了一层判断,只有是watcher实例,才能被加入到数组中去!!!!

通过这三个例子的对比,我们不难发现,在实际中,只有Watcher才会被当做依赖收集进入Dep中去,那么问题来了 ,watcher是什么被创建的呢,也就是什么时候,watcher才会被收集进入数组呢

组件挂载,watch选项,computed选项中的时候,都有new Watcher的环节

接下来我们来封装一个Observer类,用来将对象的所有的属性推入劫持函数中被劫持,这样我们就不用手动的一个个加了

       /**
         * 采用Observer类来实现所有属性的检测
         */
        class Observer{
            constructor(value){
                this.value=value
                if(!Array.isArray(value)){
                    this.walk(value)
                }
            }
            //将每一个属性都去劫持
            walk(obj){
                const keys = Object.keys(obj)
                for(let i = 0;i<keys.length;i++){
                    defineReactive(obj,keys[i],obj[keys[i]])
                }
            }
        }   

这样,我们就实现了一个Vue从0到1,完整的一个变化侦测,注意:以上方法只适用于对象的情况,数组的变化侦测,我们在下一节内容中再聊

完整代码示例

       /**
         * 模拟一下
         * Vue的监听是怎么实现的
         */
        //工具函数
        function remove(arr, item) {
            if (arr.length) {
                const index = arr.indexOf(item)
                if (index > -1) return arr.splice(index, 1)
            }
        }
        // 解析简单路径
        const bailRE = /[^\w.$]/
        function parsePath(path) {
            if (bailRE.test(path)) return
            const segments = path.split('.')
            return function (obj) {
                for (let i = 0; i < segments.length; i++) {
                    if (!obj) return
                    obj = obj[segments[i]]
                }
                console.log(obj);
                return obj
            }
        }
        /**
         * 改装劫持函数
         */
         function defineReactive(data, key, value) {
            let dep = new Dep() //
            Object.defineProperty(data, key, {
                enumerable: true, //可枚举
                configurable: true, //可配置
                get() {
                    console.log(" 进入到了get方法里");
                    dep.depend() //收集依赖
                    return value
                },
                set(newVal) {
                    console.log(" 进入到了set方法里");
                    if (value === newVal) return
                    value = newVal
                    // 触发依赖
                    dep.notify()
                }
            })
        }
        /**
         * dep类,专门用来管理依赖
         * 假设依赖是一个函数,存在window.target上
         * window.target其实就是相当于一个watcher对象
         */
        class Dep {
            //构造函数
            constructor() {
                this.subs = [] //为原型对象上的属性
            }
            // 添加数组
            addSub(sub) {
                this.subs.push(sub)
            }
            //移除数组
            removeSub(sub) {
                remove(this.subs, sub)
            }
            //收集依赖
            depend() {
                //判断是否为一个watcher类型,如果是就收集watcher
                if (window.target) {
                    console.log(window.target);
                    this.addSub(window.target)
                }
            }
            //通知组件
            notify() {
                const subs = this.subs.slice()
                console.log(subs);
                for (let i = 0, l = subs.length; i < l; i++) {
                    subs[i].updata()
                }
            }
        }
        /**
         * watcher类
         * watcher的本质就是一个实例对象
         * 会触发get方法主动将自己添加到dep中去
         * 关键问题:watcher在哪里被创造的呢?????????
         */
        class watcher {
            constructor(vm, expOrFn, cb) {
                this.vm = vm
                //判断expOrFn,也就是属性的合理性,返回一个能读取自己值得函数
                this.getter = parsePath(expOrFn)
                this.cb = cb
                this.value = this.get()
            }
            get() {
                console.log('进入watcher的到get方法里了');
                window.target = this //把watcher对象存进内存中
                let value = this.getter.call(this.vm, this.vm) //在读一下自己的值,使其加入依赖
                window.target = undefined //清空内存
                return value
            }
            updata() {
                console.log('进入watcher的到updata方法里了');
                const oldValue = this.value
                this.value = this.get()
                this.cb.call(this.vm, this.value, oldValue)
            }
        }
        /**
         * 采用Observer类来实现所有属性的检测
         */
        class Observer{
            constructor(value){
                this.value=value
                if(!Array.isArray(value)){
                    this.walk(value)
                }
            }
            //将每一个属性都去劫持
            walk(obj){
                const keys = Object.keys(obj)
                for(let i = 0;i<keys.length;i++){
                    defineReactive(obj,keys[i],obj[keys[i]])
                }
            }
        }         

        // 定义一个普通的对象
        const vue = {
                val1: "1",
                val2: "2"
        }
        new Observer(vue)//实现全数据的劫持

        console.log(vue.val1);
        vue.val1=3//触发依赖
        //  obj.val2=3//触发依赖

总结

到现在,我们就完成了Vue2对对象的一个变化的侦测,在这里,我们接触到了一些开发中听不到的名词,以及对vue数据的一个变化过程有了更深的理解,Vue不是变魔术,它的一切都是有迹可循的,这样就可以在日常的开发中,更精准的确认到一些隐晦难明的BUG

参考范文

深入浅出Vue.js

juejin.cn/post/696276… 唐某人的实现一个简单Vue3

蟹蟹各位。