资讯专栏INFORMATION COLUMN

vue - 响应式原理梳理(一)

weknow619 / 1705人阅读

摘要:问题为什么修改即可触发更新和的关联关系官方介绍的官网文档,对响应式属性的原理有一个介绍。因此本文在源码层面,对响应式原理进行梳理,对关键步骤进行解析。

描述

 我们通过一个简单的 Vue应用 来演示 Vue的响应式属性

html:
    
{{message}}
js: let vm = new Vue({ el: "#app", data: { message: "123" } })

 在应用中,message 属性即为 响应式属性

 我们通过 vm.message, vm.$data.message, 可访问 响应式属性 message

 当我们通过修改 vm.message(vm.message = "456"), 修改后的数据会 更新到UI界面中

问题

为什么修改 vm.message, 即可触发 UI更新

vm.messagedata.message 的关联关系;

官方介绍

  vue的官网文档,对响应式属性的原理有一个介绍。

把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。    

每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

 官方文档

  以上介绍,只是对响应式原理进行了简单描述,并没有深入细节。因此本文在源码层面,对响应式原理进行梳理,对关键步骤进行解析。

  响应式原理涉及到的关键步骤如下:

构建vue实例

vue实例data属性初始化,构建响应式属性

将vue实例对应的template编译为render函数

构建vue实例的watcher对象

执行render函数,构建VNode节点树,同时建立响应式属性和watcher对象的依赖关系

将VNode节点渲染为dom节点树

修改响应式属性,触发watcher的更新,重新执行render函数,生成新的VNode节点树

对比新旧Vnode,重新渲染dom节点树

构造函数 - Vue

Vue.js 给我们提供了一个 全局构造函数 Vue

 通过 new Vue(options) 生成一个 vue实例,从而可以构建一个 Vue应用

 其中,options 为构造vue实例的配置项,即为 { data, methods, computed, filter ... }

    /*
      options:
      {
        data: {...},
        methods: {...},
        computed: {...},
        watch: {...}
        ...
      }
    */
    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")
      }
      
      // 根据options, 初始化vue实例
      this._init(options)
    }
    
    export default Vue;

vue实例 构造完毕之后,执行实例私有方法 _init(), 开始初始化。

  在一个 vue应用 中,存在两种类型的 vue实例根vue实例组件vue实例

根vue实例,由构造函数 Vue 生成。

组件vue实例,由组件构造函数 VueComponent 生成,组件构造函数 继承 自构造函数 Vue

    // 全局方法extend, 会返回一个组件构造函数。
    Vue.extend = function(options) {
        ...
        
        // 组件构造函数,用于创建组件
        var Sub = function VueComponent(options) {
            this._init(options);
        };
        // 子类的prototype继承自Vue的prototype
        // 相当于Sub实例可以使用Vue实例的方法
        Sub.prototype = Object.create(Vue.prototype);
        
        ...
        
        return Sub;
    }

  通过一个 根vue实例 和多个 组件vue实例,构成了整个 Vue应用

Vue.prototype._init

  在_init方法中,vue实例会执行一系列初始化操作。

  在初始化过程中, 我们通过全局方法 initState 来初始化vue实例的 datapropsmethodscomputedwatch 属性。

     Vue.prototype._init = function(options) {
        var vm = this;
        
        ...  // 其他初始化过程, 包括建立子vue实例和父vue实例的对应关系、给vue实例添加自定义事件、执行beforeCreated回调函数等
        
        // 初始化props属性、data属性、methods属性、computed属性、watch属性
        initState(vm);
        
        ... // 其他初始化过程,比如执行created回调函数
        
        // vue实例初始化完成以后,挂载vue实例,将模板渲染成html
        if(vm.$options.el) {
                vm.$mount(vm.$options.el);
        }
    };

    function initState (vm: Component) {
      vm._watchers = [];
      // new Vue(options) 中的 options
      const opts = vm.$options; 
      
      // 将props配置项中属性转化为vue实例的响应式属性
      if (opts.props) initProps(vm, opts.props); 
      
      // 将 methods配置项中的方法添加到 vue实例对象中
      if (opts.methods) initMethods(vm, opts.methods);
      
      // 将data配置项中的属性转化为vue实例的响应式属性
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      ...
    }

  其中,initData 方法会将 data配置项 中的属性全部转化为 vue实例响应式属性

initData

initData 方法的主要过程:

根据data配置项,创建vue实例的私有属性: _data

通过 observe 方法,将 _data 对象中的属性转化为 响应式属性

通过全局方法proxy, 建立 vue实例_data 的关联关系。

    function initData(vm) {
        // 获取data配置项对象
        var data = vm.$options.data;
        // 组件实例的data配置项是一个函数
        data = vm._data = typeof data === "function"? getData(data, vm): data || {};
        
        // 获取data配置项的属性值
        var keys = Object.keys(data);
        // 获取props配置项的属性值
        var props = vm.$options.props;
        // 获取methods配置项的属性值;
        var methods = vm.$options.methods;
        var i = keys.length;
        
        while(i--) {
            var key = keys[i];
            {
                // methods配置项和data配置项中的属性不能同名
                if(methods && hasOwn(methods, key)) {
                    warn(
                        ("method "" + key + "" has already been defined as a data property."),
                        vm
                    );
                }
            }
            // props配置项和data配置项中的属性不能同名
            if(props && hasOwn(props, key)) {
                "development" !== "production" && warn(
                    "The data property "" + key + "" is already declared as a prop. " +
                    "Use prop default value instead.",
                    vm
                );
            } else if(!isReserved(key)) { // 如果属性不是$,_ 开头(vue的保留属性)
                // 建立 vue实例 和 _data 的关联关系性
                proxy(vm, "_data", key);
            }
        }
        // 观察data对象, 将对象属性全部转化为响应式属性
        observe(data, true /* asRootData */);
    }
observe

  全局方法 observe 的作用是用来观察一个对象,将_data对象的属性全部转化为 响应式属性

    // observe(_data, true)
    function observe(value, asRootData) {
        if(!isObject(value)) {
            return
        }
        var ob;
       ...
       // 
       ob = new Observer(value);
       
       ...
       
       return ob;
    }


    var Observer = function Observer(value) {
        ...
        
        if(Array.isArray(value)) {
            // 如果value是数组,对数组每一个元素执行observe方法
            this.observeArray(value);
        } else {
            // 如果value是对象, 遍历对象的每一个属性, 将属性转化为响应式属性
            this.walk(value);
        }
    };
    
    // 如果要观察的对象时数组, 遍历数组,然后调用observe方法将对象的属性转化为响应式属性
    Observer.prototype.observeArray = function observeArray(items) {
        for(var i = 0, l = items.length; i < l; i++) {
            observe(items[i]);
        }
    };
    
    
     // 遍历obj的属性,将obj对象的属性转化为响应式属性
    Observer.prototype.walk = function walk(obj) {
        var keys = Object.keys(obj);
        for(var i = 0; i < keys.length; i++) {
           // 给obj的每一个属性都赋予getter/setter方法。
           // 这样一旦属性被访问或者更新,这样我们就可以追踪到这些变化
            defineReactive(obj, keys[i], obj[keys[i]]);
        }
    };
defineReactive

  通过 defineProperty 方法, 提供属性的 getter/setter 方法。

读取 属性时,触发 getter,将与响应式属性相关的vue实例保存起来。

修改 属性时,触发 setter,更新与响应式属性相关的vue实例。

    function defineReactive(obj, key, val, customSetter, shallow) {
        // 每一个响应式属性都会有一个 Dep对象实例, 该对象实例会存储订阅它的Watcher对象实例
        var dep = new Dep();
        
        // 获取对象属性key的描述对象
        var property = Object.getOwnPropertyDescriptor(obj, key);
        
        // 如果属性是不可配置的,则直接返回
        if(property && property.configurable === false) {
            return
        }

        // 属性原来的getter/setter
        var getter = property && property.get;
        var setter = property && property.set;
        
        // 如果属性值是一个对象,递归观察属性值,
        var childOb = !shallow && observe(val);
        
        // 重新定义对象obj的属性key
        Object.defineProperty(obj, key, {
            enumerable : true,
            configurable : true,
            get : function reactiveGetter() {
                // 当obj的某个属性被访问的时候,就会调用getter方法。
                var value = getter ? getter.call(obj) : val;
                
                
                // 当Dep.target不为空时,调用dep.depend 和 childOb.dep.depend方法做依赖收集
                if(Dep.target) {
                
                    // 通过dep对象, 收集依赖关系
                    dep.depend();
                    if(childOb) {
                        childOb.dep.depend();
                    }
                    // 如果访问的是一个数组, 则会遍历这个数组, 收集数组元素的依赖
                    if(Array.isArray(value)) {
                        dependArray(value);
                    }
                }
                return value
            },
            set : function reactiveSetter(newVal) {
                // 当改变obj的属性是,就会调用setter方法。这是就会调用dep.notify方法进行通知
                var value = getter ? getter.call(obj) : val;
                /* eslint-disable no-self-compare */
                if(newVal === value || (newVal !== newVal && value !== value)) {
                    return
                }
                /* eslint-enable no-self-compare */
                if("development" !== "production" && customSetter) {
                    customSetter();
                }
                if(setter) {
                    setter.call(obj, newVal);
                } else {
                    val = newVal;
                }
                childOb = !shallow && observe(newVal);
                // 当响应式属性发生修改时,通过dep对象通知依赖的vue实例进行更新
                dep.notify();
            }
        });
    }

  响应式属性, 通过一个 dep 对象, 收集依赖响应式属性的vue实例,在属性改变时 通知vue实例更新

  一个 响应式属性, 对应一个 dep 对象。

Dep

  在观察者设计模式中,有两种角色:SubjectObserver

Subject 会维护一个 Observer的依赖列表。当 Subject 发生变化时,会通知 Observer 更新。

  在vue中,响应式属性作为Subject, vue实例作为Observer, 响应式属性的更新会通知vue实例更新。

  响应式属性通过 dep 对象来收集 依赖关系 。一个响应式属性,对应一个dep对象。

    var Dep = function Dep() {
        // dep对象的id
        this.id = uid++;
        // 数组,用来存储依赖响应式属性的Observer
        this.subs = [];
    };
    
    // 将Observer添加到dep对象的依赖列表中
    Dep.prototype.addSub = function addSub(sub) {
        // Dep对象实例添加订阅它的Watcher
        this.subs.push(sub);
    };
    
    // 将Observer从dep对象的依赖列表中删除
    Dep.prototype.removeSub = function removeSub(sub) {
        // Dep对象实例移除订阅它的Watcher
        remove(this.subs, sub);
    };
    
    // 收集依赖关系
    Dep.prototype.depend = function depend() {
        // 把当前Dep对象实例添加到当前正在计算的Watcher的依赖中
        if(Dep.target) {
            Dep.target.addDep(this);
        }
    };
    
    // 通知Observer更新
    Dep.prototype.notify = function notify() {
        // stabilize the subscriber list first
        var subs = this.subs.slice();
        // 遍历所有的订阅Watcher,然后调用他们的update方法
        for(var i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    };
proxy

  通过 defineProperty 方法, 给vue实例对象添加属性,提供属性的 getter/setter 方法。

  读取vue实例的属性( data配置项中的同名属性 ), 触发 getter,读取 _data 的同名属性。

  修改vue实例的属性( data配置项中的同名属性 ), 触发 setter,修改 _data 的同名属性。

    // proxy(vm, _data, "message")
    function proxy(target, sourceKey, key) {
        sharedPropertyDefinition.get = function proxyGetter() {
            return this[sourceKey][key]
        };
        sharedPropertyDefinition.set = function proxySetter(val) {
            this[sourceKey][key] = val;
        };
        Object.defineProperty(target, key, sharedPropertyDefinition);
    }

  通过 proxy 方法,vue实例 可代理私有属性 _data, 即通过 vue实例 可以访问/修改 响应式属性

总结

  结合源码理解, 响应式属性 的原理为:

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101374.html

相关文章

  • vue - 响应原理梳理(二)

    摘要:原型方法通过原型方法方法来挂载实例。当响应式属性发生变化时,会通知依赖列表中的对象进行更新。此时,对象执行方法,重新渲染节点。在执行过程中,如果需要读取响应式属性,则会触发响应式属性的。总结响应式属性的原理 vue实例 初始化 完成以后,接下来就要进行 挂载。 vue实例挂载,即为将vue实例对应的 template模板,渲染成 Dom节点。 原型方法 - $mount   通过原...

    mochixuan 评论0 收藏0
  • JS核心知识点梳理——原型、继承(下)

    摘要:引言上篇文章介绍原型,这篇文章接着讲继承,呕心沥血之作,大哥们点个赞呀明确一点并不是真正的面向对象语言,没有真正的类,所以我们也没有类继承实现继承有且仅有两种方式,和原型链在介绍继承前我们先介绍下其他概念函数的三种角色一个函数,有三种角色。 showImg(https://segmentfault.com/img/bVbo4hv?w=1800&h=1000); 引言 上篇文章介绍原型,...

    joyqi 评论0 收藏0
  • vue总结系列--数据驱动和响应

    摘要:由于是需要兼容的后台系统,该项目并不能使用到等技术,因此我在上的经验大都是使用原生的编写的,可以看见一个组件分为两部分视图部分,和数据部分。 在公司里帮项目组里开发后台系统的前端项目也有一段时间了。 vue这种数据驱动,组件化的框架和react很像,从一开始的快速上手基本的开发,到后来开始自定义组件,对element UI的组件二次封装以满足项目需求,期间也是踩了不少坑。由于将来很长一...

    AbnerMing 评论0 收藏0
  • Vue 数据响应原理

    摘要:接下来,我们就一起深入了解的数据响应式原理,搞清楚响应式的实现机制。回调函数只是打印出新的得到的新的值,由执行后生成。及异步更新相信读过前文,你应该对响应式原理有基本的认识。 前言 Vue.js 的核心包括一套响应式系统。 响应式,是指当数据改变后,Vue 会通知到使用该数据的代码。例如,视图渲染中使用了数据,数据改变后,视图也会自动更新。 举个简单的例子,对于模板: {{ name ...

    Mike617 评论0 收藏0
  • 浅谈Vue中计算属性computed的实现原理

    摘要:虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。当某个属性发生变化,触发拦截函数,然后调用自身消息订阅器的方法,遍历当前中保存着所有订阅者的数组,并逐个调用的方法,完成响应更新。 虽然目前的技术栈已由Vue转到了React,但从之前使用Vue开发的多个项目实际经历来看还是非常愉悦的,Vue文档清晰规范,api设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多...

    laznrbfe 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<