vue响应式原理 源码解析

引言

从状态生成dom再输出到用户界面显示的流程叫做渲染,应用在运行时会不断进行重新渲染。而响应式系统赋予框架重新渲染的能力。变化侦测的作用就是侦测到数据的变化,当数据变化时,会通知视图进行相应的更新。

追踪变化

Vue.js的响应式原理依赖于Object.defineProperty,这个方法就是用来追踪变化的,该方法的更详细的MDN说明文档在Object.defineProperty

Object.defineProperty

Object.defineProperty(obj, prop, descriptor)

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

前面两个比较好理解,即对象和属性。最后的属性描述符需要另说明。

属性描述符分为

  1. 数据描述符 :数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的
  2. 存取描述符:存取描述符是由getter-setter函数对描述的属性

描述符必须是这两种形式之一;不能同时是两者
因为源码里没有用到数据描述符,就不说了

描述符

数据描述符和存取描述符均具有以下可选键值

  • configurable
    当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
  • enumerable
    当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。

存取描述符同时具有以下可选键值:

  • get
    一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入。默认为 undefined。
  • set
    一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。 undefined。

追踪变化源码

通过对Object.defineProperty的学习,再去理解追踪变化部分的源码就比较好理解了

function defineReactive (obj, key, val, cb) {
    Object.defineProperty(obj, key, {
        enumerable: true,//该属性出现在对象的枚举属性中
        configurable: true,//该属性描述符可以被改变
        get: ()=>{
            /*....依赖收集等....*/
            /*Github:https://github.com/answershuto*/
            return val
        },
        set:newVal=> {
            val = newVal;
            cb();/*订阅者收到消息的回调*/
        }
    })
}

即追踪obj的key属性的变化,每当从obj的key中读取数据时,get函数被触发;每当往data的key中设置数据时,set函数被触发。
vue主要就是利用了每次改变对象属性,set函数都会对其进行一些处理的特性。

追踪所有属性的变化

前面介绍的代码只能侦测数据中某一个属性,如果希望把数据中的所有属性,包括子属性都侦测到,要封装一个observer,作用是将一个数据内的所有属性(包括子属性)都转换成getter/setter形式,然后去追踪他们的变化。解释在注释上。

function observe(value, cb) {//遍历对象中的所有属性 执行defineReactive
    Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
}

function defineReactive (obj, key, val, cb) {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{
            /*....依赖收集等....*/
            /*Github:https://github.com/answershuto*/
            return val
        },
        set:newVal=> {
            val = newVal;
            cb();/*订阅者收到消息的回调*/
        }
    })
}

class Vue {
    constructor(options) {//类的构造函数,options是new Vue对象时候的传参,传入的是一个对象{}
        this._data = options.data;//对象.data=>{text:'text',text2:'text2'}
        observe(this._data, options.render)//将data中的所有属性遍历变为可观察的。options.render 渲染的回调函数
    }
}

let app = new Vue({//new一个vue对象  传入一个对象 下面是键值对
    el: '#app',
    data: {
        text: 'text',
        text2: 'text2'
    },
    render(){
        console.log("render");
    }
})

解释都在注释里面,为了更好理解说一下Object.keys,详细文档链接如下Object.keys。Object.keys也是返回该对象中的所有key组成的数组。

    Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))

这行代码的意思也就是:先取得value下面的所有key(属性),比如【text,text2】。遍历该数组,执行defineReactive(value, key, value[key] , cb)。即可将data对象中所有属性转成getter/setter模式
一层一层看下来,只要vue对象里的__data中某个属性改变,就会触发set。即对订阅者进行回调(在这里是render),也就是视图更新。
这里需要对class有理解,具体见js class

proxy

还有个问题是,从上面代码可以看出,需要对app._data.text操作才会触发set。为了偷懒,我们需要一种方便的方法通过app.text直接设置就能触发set对视图进行重绘。也就是我们平时用vue时候用到this.XXX=xxx,而不是this.data.XXX=xxx。那么就需要用到代理。
可以在Vue的构造函数constructor中为data执行一个代理proxy。这样就把data上面的属性代理到了vm实例上。

_proxy.call(this, options.data);/*构造函数中*/ 
//因为在构造函数里面。第一个传参是构造函数的this,构造函数的this就是即将生成的对象。
//可以理解为_proxy.call(即将生成的对象, options.data)
//call的作用是指定函数中的this指向第一个传参。这句代码的意思就是指定proxy中的this指向‘构造函数即将生成的对象’。options.data是传入proxy的参数
/*代理*/
function _proxy (data) {
    const that = this;//即将生成的vue对象 可以理解为app
    Object.keys(data).forEach(key => {//data是options.data
        Object.defineProperty(that, key, {// that=>app key=>eg. text
            configurable: true,
            enumerable: true,
            get: function proxyGetter () {//取值时
                return that._data[key]; //app.text = app._data.text
            },
            set: function proxySetter (val) {//设值时
                that._data[key] = val; //app._data.text = val
            }
        })
    });
}

猜你喜欢

转载自blog.csdn.net/qq_33712668/article/details/93893307