• 【Vue源码学习】响应式原理探秘


    最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来。我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作。这里先来学习Vue的响应式原理,Vue2.0的响应式原理是基于Object.defineProperty来实现的。Vue通过对传入的数据对象属性的getter/setter方法来监听数据的变化,通过getter进行依赖收集,setter方法通知观察者,在数据变更时更新视图。

    1.使用rollup搭建开发环境

    安装rollup环境

    npm i @babel/preset-env @babel/core rollup rollup-plugin-babel rollup-plugin-serve cross-env -D
    

    配置rollup

    // rollup.config.js
    import babel from "rollup-plugin-babel"
    import serve from "rollup-plugin-serve"
    
    
    export default {
        input: './src/index.js',  // 打包入口
        output: {
            file: 'dist/umd/vue.js', //出口路径
            name: 'Vue' , // 指定打包后全局变量的名字
            format: 'umd' , // 统一模块规范
            sourcemap: true, // es6->es5 开启源码调试,可以找到源代码报错位置
        },
        plugins:[ //使用的插件
            babel({
                exclude:'node_modules/**' //排除文件
            }),
            process.env.ENV==='development'?serve({
                open:true,
                openPage:'/public/index.html', //默认启动html的路径
                port:3000,
                contentBase: ''
            }):null
        ]
    }
    

    项目搭建

    这里搭建了一个Vue项目,主要代码都放在src下面

    40B5B58D-2758-4C50-821E-91DA89794F23.png

    2.响应式原理探秘

    1.Object.defineProperty

    想要了解Vue2的响应式原理,我们得先来简单了解一下Object.defineProperty

    Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改(immutable)的。

    Object.defineProperty(obj,prop,desc)
    
    • obj:需要定义属性的对象
    • prop:当前需要定义的对象属性
    • desc:属性描述符

    该方法最低兼容到IE8,这也就是Vue最低兼容到IE8的原因。

    2.Vue初始化过程

    我们先来分析一下Vue的初始化都做了哪些事情,我们在使用Vue的时候一般都会这样写:

    const vm = new Vue({
      el:'#app',
      data(){
        return {
          name: '南玖'
        }
      }
    })
    

    我们知道Vue只能通过new关键字初始化,所以Vue应该是一个构造函数,然后会调用this._init方法进行初始化过程,OK,我们自己可以来实现一下

    import {initMixin} from "./init"
    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      } // 开发环境下不通过new进行调用会告警
      /*调用_init初始化,这个方法是挂在Vue原型上的*/
      this._init(options)
      // options就是new Vue是传入的参数,包括:el,data,computed,watch,methods...
    }
    
    initMixin(Vue) // 给Vue原型上添加_init方法
    export default Vue
    

    我们接着来写这个init.js,这里主要是给Vue原型上挂上方法:_init,$mount,_render,$nextTick

    import {initState} from "./state"
    
    //initMixin就做了一件事,就是给Vue原型挂上_init方法
    export function initMixin(Vue){
        // 初始化流程
        Vue.prototype._init = function (options){
            // console.log(options)
            const vm = this // vue中使用this.$options
            vm.$options = options
    
            // /*初始化props、methods、data、computed与watch*/
            initState(vm) 
          // 这里先看initState,后面还会有很多初始化事件:初始化生命周期、初始化事件、初始化render等等
        }
    }
    

    初始化data,这里我们知道Vue支持传入的data可以是一个对象也可以是一个方法,所以我们需要判断一下传入的data的数据类型,是对象就直接传给observe,是方法就先执行再将返回值传给observe

    function initData(vm) {
        console.log('初始化数据',vm.$options.data)
        // 数据初始化
        let data = vm.$options.data;
        data = vm._data =  typeof data === 'function' ? data.call(this) : data
        // 对象劫持,用户改变了数据 ==》 刷新页面
        // MVVM模式 数据驱动视图
    
        // Object.definePropety() 给属性增加get和set方法
        observe(data)  //响应式原理
    }
    

    3.响应式原理

    将数据变成可观察的,我们都知道Vue2是通过Object.defineProperty来实现的。ok,这里我们就进入了这次的重点原理讲解:我们知道Object.defineProperty这个方法,只能劫持对象不能劫持数组,所以这里我们判断一下数据类型,数组需要单独处理,重写数组原型上的方法,在数组变更时在通知到订阅者

    // 把data中数据使用Object.defineProperty重新定义 es5
    // Object.defineProperty 不能兼容IE8及以下,所以vue2无法兼容IE8版本
    import {isObject,def} from "../util/index"
    import {arrayMethods} from "./array.js"  // 数组方法
    export function observe (data) {
        // console.log(data,'observe')
        let isObj = isObject(data)
        if(!isObj) return 
        return new Observer(data) // 观测数据
    }
    
     class Observer {
         constructor(v){
            // 如果数据层次过多,需要递归去解析对象中的属性,依次增加set和get方法
            def(v,'__ob__',this)
            if(Array.isArray(v)) {
                // 如果是数组的话并不会对索引进行监测,因为会导致性能问题
                // 前端开发中很少去操作索引 push shift unshift
                v.__proto__ = arrayMethods
                // 如果数组里放的是对象,再进行监测
                this.observerArray(v)
            }else{
              //对象则调用walk进行劫持
                this.walk(v)
            }
            
         }
         observerArray(value) {
             for(let i=0; i<value.length;i++) {
                 observe(value[i])
             }
         }
       /* 遍历每一个对象并且为它们绑定getter与setter。该方法只有在数据类型为对象时才能被调用  */
         walk(data) {
             let keys = Object.keys(data); //获取对象key
             keys.forEach(key => {
                defineReactive(data,key,data[key]) // 定义响应式对象
             })
         }
     }
    
     function  defineReactive(data,key,value){
         observe(value) // 递归实现深度监测,注意性能
         Object.defineProperty(data,key,{
             get(){
                 // 依赖收集,下期探讨
                 //获取值
                return value
             },
             set(newV) {
                 //设置值
                if(newV === value) return
                observe(newV) //继续劫持newV,用户有可能设置的新值还是一个对象
                value = newV
               /*dep对象通知所有的观察者,下期探讨*/
          			//dep.notify()
                console.log('值变化了',value)
             }
         })
     }
    

    4.数组方法重写

    
    // 重写数组的7个方法: push,pop,shift,unshift,reverse,sort,splice会导致数组本身改变
    
    let oldArrayMethods = Array.prototype
    // value.__proto__ = arrayMethods 
    // arrayMethods.__proto__ = oldArrayMethods
    export let arrayMethods = Object.create(oldArrayMethods)
    
    const methods = [
        'push','pop','shift','unshift','reverse','sort','splice'
    ]
    
    methods.forEach(method=>{
        arrayMethods[method] = function(...args) {
            console.log('用户调用了:'+method,args)
            const res = oldArrayMethods[method].apply(this, args) // 调用原生数组方法
            // 添加的元素可能还是一个对象
    
            let inserted = args //当前插入的元素
            //数组新插入的元素需要重新进行observe才能响应式
            let ob = this.__ob__
            switch (method) {
                case 'push':
                case 'unshift':
                    inserted = args
                    break;
                case 'splice':
                    inserted = args.slice(2)
                    break;
                default:
                    break;
            }
            if(inserted) {
                ob.observerArray(inserted)  //将新增属性继续
            }
    
            console.log('数组更新了:'+ JSON.stringify(inserted))
            //通知所有注册的观察者进行响应式处理,这里下期再来探讨
            // ob.dep.notify() 
            return res
        }
    })
    
    

    OK,写到这里我们可以来测试一下我们的Vue了

    let vm = new Vue({
      el:'#app',
      data(){
        return{
          a:1,
          b:{name:'nanjiu'},
          c:[{name:'front end'}]
        }
      },
      computed:{}
    })
    vm._data.a = 2
    vm._data.c.push({name:'sss'})
    

    这里控制台应该会打印出如下内容:

    数组重写.png

    这样Vue的数据响应式,我们就算实现了,但这里看着有点别扭,我们希望操作Vue的data里的数据可以直接通过this来获取,而不是通过this._data来获取,这个很简单,我们只需要再做一层代理就可以实现了。

    5.代理

    export function proxy (target,sourceKey,key) {
        // target: 想要代理到的目标对象,sourceKey:想要代理的对象
      	const _that = this
        Object.defineProperty(target, key, {
            enumerable: true,
            configurable: true,
            get: function(){
                return _that[sourceKey][key]
            },
            set: function(v){
                _that[sourceKey][key] = v
            }
        })
    }
    

    然后再initData里面调用该方法

    function initData(vm) {
        console.log('初始化数据',vm.$options.data)
        // 数据初始化
        let data = vm.$options.data;
        data = vm._data =  typeof data === 'function' ? data.call(this) : data
        // 对象劫持,用户改变了数据 ==》 刷新页面
        // MVVM模式 数据驱动视图
         Object.keys(data).forEach(i => {
            proxy.call(vm,vm,'_data',i)
        })
        // Object.definePropety() 给属性增加get和set方法
        observe(data)  //响应式原理
    }
    

    然后我们就可以愉快的使用this直接去访问data里面的数据了~

    3.总结

    OK,Vue的响应式原理我们就算全都实现了一遍,Vue2的响应式原理主要是通过Object.defineProperty来实现的,但这个方法有缺陷,不能劫持数组,所以对数据需要单独处理,在Vue3中,底层把响应式处理改成了通过proxy来实现,这个方法对数组劫持也同样适用。这里我们只探讨了Vue是如何进行响应式处理,至于它如何收集依赖,以及如何通知视图更新我们下期再来一起学习吧~

    觉得文章不错,可以点个赞呀_ 另外欢迎关注留言交流~

  • 相关阅读:
    How Default Heap Of Process Grows
    希腊字母表
    Ubuntu第一次亲密接触
    Ubuntu中的挂载点(mount point)
    要一专多能
    First touch with JIT debugging
    小学一下环境变量
    安装VMware Tools
    [转]ReiserFS与ext3的比较
    [bbk4485]第二章Flashback Database 05
  • 原文地址:https://www.cnblogs.com/songyao666/p/15838356.html
Copyright © 2020-2023  润新知