vue源码分析【3】- vue响应式

469 阅读20分钟

当前篇:vue源码分析【3】-vue响应式

以下代码和分析过程需要结合vue.js源码查看,通过打断点逐一比对。

模板代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="./../../oldVue.js"></script>
</head>

<body>
    <div id="app">
        <h2>开始存钱</h2>
        <div>每月存 :¥{{ money }}</div>
        <div>存:{{ num }}个月</div>
        <div>总共存款: ¥{{ total }}</div>
        <button @click="getMoreMoney">{{arryList[0].name}}多存一点</button>
        <msg-tip :msginfo='msgText' :totalnum='total'></msg-tip>
    </div>

    <script>
        debugger;
        // 定义一个新组件
        var a =  {
            props:['msginfo', 'totalnum'],
            data: function () {
                return {
                    count: 0
                }
            },
            template: '<div>{{ msginfo }}存了¥{{ totalnum }}</div>'
        }

        var app = new Vue({
            el: '#app',
            components: { msgTip: a},
            beforeCreate() { },
            created() { },
            beforeMount() { },
            mounted: () => { },
            beforeUpdate() { },
            updated() { },
            beforeDestroy() { },
            destroyed() { },
            data: function () {
                return {
                    money: 100,
                    num: 12,
                    arryList: [{name:'子树'}],
                    msgText: "优秀的乃古:"
                }
            },
            computed: {
                total() {
                    return this.money * this.num;
                }
            },
            watch:{  // 这里一开始没有写,在watch章节才加的
                money:{
                    handler(newVal, oldVal){
                        this.msgText = newVal+this.msgText
                    },
                    deep:true,
                    immediate:true
                }
            },
            methods: {
                getMoreMoney() {
                    this.money = this.money * 2
                    this.arryList.unshift({name: '大树'})
                }
            }
        })

    </script>

</body>

</html>

1. 前言

本文的结构依据点,线,面来展开。

  • 点即函数的作用
  • 线即函数的执行流程
  • 面即源码的详细解读

十分不建议直接看源码,很多函数非常长,并且链路很长,在没有对函数有大概的了解情况,大概率下,你读了一遍源码后会发现,wc 我刚看了源码了吗?可是咋记不清它们做了啥操作。因此,先看作用,再看流程,再展开看源码。


2. 概念

2-1. 什么是Vue响应式呢

数据发生变化后,会重新对页面渲染,这就是Vue响应式

2-2. 想完成这个过程,我们需要做些什么

  • 侦测数据的变化
  • 收集视图依赖了哪些数据
  • 数据变化时,自动“通知”需要更新的视图部分,并进行更新

它们对应专业俗语分别是:

  • 数据劫持 / 数据代理
  • 依赖收集
  • 发布订阅模式

3-3. 翻译翻译

张麻子:数据发生变化,会重新渲染,你给翻译翻译

师爷:我们肯定要知道哪些数据发生了变化吧(数据劫持),光知道哪些数据变化了还没用,还要知道视图用到了哪些变化的数据吧(依赖收集),最后,还得告诉页面,你依赖的数据变化了,赶紧更新吧(发布订阅模式)

4-4. 名词解析

为了让大家能够更好的阅读,介绍几个基本的概念。

  • Watcher: 观察者
    • 我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
  • Dep: 订阅者
    • 收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。Dep用于解耦属性的依赖收集和派发更新操作,「说得具体点」:它的主要作用是用来存放 Watcher 观察者对象。

image.png

3. 整体过程

image.png

Vue实例化一个对象的具体过程如下

  1. 新创建一个实例后,Vue调用compile将el转换成vnode。
  2. 调用initState, 创建props, data的钩子以及其对象成员的Observer(添加getter和setter)。
  3. 执行mount挂载操作,在挂载时建立一个直接对应render的Watcher,并且编译模板生成render函数,执行vm._update来更新DOM。
  4. 每当有数据改变,都将通知相应的Watcher执行回调函数,更新视图。
    • 当给这个对象的某个属性赋值时,就会触发set方法。
    • set函数调用,触发Dep的notify()向对应的Watcher通知变化。
    • Watcher调用update方法。

在这个过程中:

  • Observer是用来给数据添加Dep依赖。
  • Dep是data每个对象包括子对象都拥有一个该对象, 当所绑定的数据有变更时, 通过dep.notify()通知Watcher。
  • Compile是HTML指令解析器,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
  • Watcher是连接Observer和Compile的桥梁,Compile解析指令时会创建一个对应的Watcher并绑定update方法 , 添加到Dep对象上。
  data:{
      Dep:{
          watch1,
          watch2,
      }
  }

3. initState

3-1. 基本信息

初始化状态
new Vue 后会开始,执行function initMixin,来初始化各个状态。

作用:

  • 初始化状态: 分别初始化props、methods、data、computed、watch,它也是数据响应式的入口
  • 优先级props、methods、data、computed 对象中的属性不能出现重复,优先级和列出顺序一致,其中 computed 中的 key 不能和 props、data 中的 key 重复,methods 不影响

源码:

    function initState(vm) {
        vm._watchers = []; // 初始化观察者队列
        var opts = vm.$options; // 初始化参数
        // 判断是否有props属性,如果有则添加观察者,此例我们没有在加props属性
        // 那么opts.props是在哪里挂载的,实际是在initMixin的时候,vm.$options = mergeOptions()
        // 这里挂载的
        if (opts.props) {
            //初始化props 检验props 数据格式是否是规范的如果是规范的则添加到观察者队列中
            initProps(vm, opts.props);
        }
        if (opts.methods) {
            //   初始化事件Methods 把事件 冒泡到 vm[key] 虚拟dom  最外层中
            initMethods(vm, opts.methods);
        }
        if (opts.data) {
            // 初始化数据 获取options.data 的数据 将他们添加到 监听者中
            initData(vm);
        } else {
            //  判断value 是否有__ob__    实例化 dep对象,获取dep对象  为 value添加__ob__ 属性,
            // 把vm._data添加到观察者中  返回 new Observer 实例化的对象
            observe(vm._data = {}, true /* asRootData */);

        }
        if (opts.computed) { //计算属性
            //初始化计算属性 并且判断属性的key 是否 在 data ,将 计算属性的key 添加入监听者中
            initComputed(vm, opts.computed);
        }
        //options 中的 watch
        if (opts.watch && opts.watch !== nativeWatch) {
            //初始化Watch
            initWatch(vm, opts.watch);
        }
    }

3-2. initProps

作用:

初始化数据 获取options.props 的数据, 为 props 对象的每个属性设置响应式,并将其代理到 vm 实例上

源码:

if (opts.props) {
      //初始化props 检验props 数据格式是否是规范的如果是规范的则添加到观察者队列中
      initProps(vm, opts.props);
}

//初始化props 检验props 数据格式是否是规范的如果是规范的则添加到观察者队列中
function initProps(
        vm, 
        propsOptions //props对象 例:{ msginfo: {type: null}, totalnum: {type: null} }
                    // 变成这种格式,是normalizeProps函数处理得到的
        ) {
        debugger
        // 例:{ msginfo: "优秀的乃古:",totalnum: 1200 }
        var propsData = vm.$options.propsData || {}; 
        var props = vm._props = {}; // 挂载_props
        var keys = vm.$options._propKeys = []; // 挂载_propKeys
        var isRoot = !vm.$parent; // $parent属性存在,此节点就不是根节点
        // 设置不监听 观察者
        if (!isRoot) {  
            toggleObserving(false);  // shouldObserve = false;
        }
        // 遍历函数,传入props的key
        var loop = function (key) {
            debugger
            ...
        };
        //循环校验 props 是否 是合格数据 并且添加观察者
        for (var key in propsOptions) loop(key);
        toggleObserving(true);
}

loop:

获取 props[key] 的默认值,并对prop进行校验

var loop = function (key) {
            debugger
            keys.push(key);
            // 获取 props[key] 的默认值
            var value = validateProp(
                key, //props 对象的key 例:"totalnum"
                propsOptions, //例:{ msginfo: {type: null}, totalnum: {type: null} }
                propsData, // 例:{ msginfo: "优秀的乃古:",totalnum: 1200 }
                vm
            );
            /* istanbul ignore else 伊斯坦布尔忽略其他 */
            {
                //大写字母,加完减号又转成小写了 比如把驼峰 aBc 变成了 a-bc
                //匹配大写字母并且两面不是空白的 替换成 - 在转换成小写
                var hyphenatedKey = hyphenate(key);
                // 检查属性是否为保留属性。
                //var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');
                if (isReservedAttribute(hyphenatedKey) ||
                    config.isReservedAttr(hyphenatedKey)) {
                    //输出警告
                    warn(
                        ("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
                        vm
                    );
                }

                //通过defineProperty的set方法去通知notify()订阅者subscribers有新的值修改
                defineReactive(props, key, value, function () {
                    if (vm.$parent && !isUpdatingChildComponent) {
                        warn(
                            "Avoid mutating a prop directly since the value will be " +
                            "overwritten whenever the parent component re-renders. " +
                            "Instead, use a data or computed property based on the prop's " +
                            "value. Prop being mutated: \"" + key + "\"",
                            vm
                        );
                    }
                });
            }
            // static props are already proxied on the component's prototype
            // during Vue.extend(). We only need to proxy props defined at
            // instantiation here.
            if (!(key in vm)) { //如果vm中没有props属性,则把他添加到vm中,这样组件this.[propsKey] 就可以获取到值了
                proxy(vm, "_props", key);
            }
};

3-2-1. validateProp

执行流程:

  • 判断prop的type是否是布尔值
  • 定义一个value为props对象中对应key的值,对其进行一系列操作,最终返回它
  • 如果type是布尔值
    • 如果key不是propsData自有属性,并且没有定义default 默认值的时候,设置value 为false
    • 如果value为空,或者value和key一样,且(type不是字符串,或者布尔值索引小于字符串的时候,说明先匹配的是布尔类型),则设置value为true
  • 如果value未定义,尝试获取默认值,对value添加观察
  • 检查prop 是否合格
  • 返回最终的value

源码:

    function validateProp(
        key, //props 对象的key 例:"totalnum"
        propOptions, //例:{ msginfo: {type: null}, totalnum: {type: null} }
        propsData, // 例:{ msginfo: "优秀的乃古:",totalnum: 1200 }
        vm
    ) {  //vm this属性

        var prop = propOptions[key]; //获取组件定义的props 属性 例:{type: null}
        var absent = !hasOwn(propsData, key);  // key是否不为自身属性
        var value = propsData[key]; // 获取值 例:"优秀的乃古:"
        /**
         * prop.type:
         * 传入prop的类型,props如果是对象形式的声明,我们一般会写它的类型type,如果是数组格式,
         * 我们就只写了各个props的key,所以数组格式的type为null
         * 
         * getTypeIndex:
         * 判断props中的type是否符合期望类型,如果符合就返回0或者下标,否则返回-1
         */
        var booleanIndex = getTypeIndex(Boolean, prop.type); 
        if (booleanIndex > -1) { // 如果是boolean值
            // 如果key 不是propsData 实例化,或者 没有定义default 默认值的时候   设置value 为false
            if (absent && !hasOwn(prop, 'default')) { 
                value = false;
            } else if (
                value === ''  // 如果value 是空
                || value === hyphenate(key) //或者key转出 - 形式和value 相等的时候
            ) { 
                // 判断prop.type 的类型是否是string字符串类型
                var stringIndex = getTypeIndex(String, prop.type);
                // 如果不是字符串类型,
                // 或者布尔值索引小于字符串的时候,说明先匹配的是布尔类型,
                // 此时将值强制转换为true
                if (
                    stringIndex < 0 ||
                    booleanIndex < stringIndex) { 
                    value = true;
                }
            }
        }
        debugger
        // 如果value未定义
        if (value === undefined) {  
             // 尝试获取默认值
            value = getPropDefaultValue(vm, prop, key);
            var prevShouldObserve = shouldObserve;
            toggleObserving(true);
            observe(value);
            toggleObserving(prevShouldObserve);
        }
        {
            //检查prop 是否合格
            assertProp(
                prop, //属性的type值
                key, //props属性中的key
                value, //view 属性的值
                vm, // VueComponent 组件构造函数
                absent //false
            );
        }
        return value
    }
3-2-1-1. getPropDefaultValue

作用:

返回prop属性默认的default值。

执行流程:

  • 如果prop上不存在default,那么直接返回undefined
  • prop的default是函数,并且type不是函数声明,则返回执行defalut函数,否则返回defalut

关联文档:

我们在写props的时候,如果我们接收的是布尔值,直接在default写true或者false没问题,如果我们接收的是数组或者对象,默认值直接写[]或者{}就会报错,平常业务中我们可能会经常不注意导致报错,例如:

props: {
                    visible: {
                        type: Boolean,
                        default: false //布尔值可以不用工厂函数
                    },
                    selectLogisticsNos: {
                        type: Array,
                        default: () => [] //Object/Array必须用工厂函数
                        // default: [] //会报错
                    },
                  }

那么为什么要这么做呢? 因为我们的对象操作的时候,需要把它们的this指向vm实例。

源码:

    function getPropDefaultValue(vm, prop, key) {
        // 判断该对象prop 中的default 是否是prop 实例化的
        if (!hasOwn(prop, 'default')) {
            return undefined
        }
        var def = prop.default;
        /**
         *  属性键的默认值无效,
            类型为Object/Array的属性必须使用工厂函数,返回默认值

            例:  props: {
                    visible: {
                        type: Boolean,
                        default: false //布尔值可以不用工厂函数
                    },
                    selectLogisticsNos: {
                        type: Array,
                        default: () => [] //Object/Array必须用工厂函数
                    },
                  }
         */
        if ("development" !== 'production' && isObject(def)) {
            warn(
                'Invalid default value for prop "' + key + '": ' +
                'Props with type Object/Array must use a factory function ' +
                'to return the default value.',
                vm
            );
        }
        // 原始PROP值也未在先前的渲染中定义,
        // 返回先前的默认值以避免不必要的监视触发器
        if (vm && vm.$options.propsData &&
            vm.$options.propsData[key] === undefined &&
            vm._props[key] !== undefined
        ) {
            return vm._props[key]
        }
        // prop的default是函数,并且type不是函数,则执行defalut函数,否则返回defalut
        return typeof def === 'function' && getType(prop.type) !== 'Function'
            ? def.call(vm)
            : def
    }

3-3. initMethods

作用:

对函数进行一系列的约束,修正methods对象,并且直接把各个函数直接挂载到vue上

执行流程:

  • 遍历methods对象
  • 对函数进行约束
    • 如果函数对象对应key的事件不存在则发出警告
    • 如果属性中定义了key,则在methods中不能定义同样的key
    • 检查一个字符串是否以$或者_开头的字母,事件不能以$或者_开头的字母
  • 把事件直接挂载到vue上,如果是函数为空则给一个空函数,如果是有函数则返回函数函数

关联文档:

我们在vue中使用事件时,直接用this.fn(),即Vue.fn()。那么为什么能直接这么用呢?

就是因为initMethods最后一步,将每个函数直接挂载到了vm上了。

源码:

if (opts.methods) {
     // 初始化事件Methods 把事件 冒泡到 vm[key] 虚拟dom  最外层中
     initMethods(vm, opts.methods);
}

function initMethods(
        vm, 
        methods //例:{ getMoreMoney: ƒ getMoreMoney() }
        ) {
        debugger 
        var props = vm.$options.props;
        //循环 methods 事件对象
        for (var key in methods) {
            {
                // 如果对应key的事件不存在则发出警告
                if (methods[key] == null) {
                    warn(
                        "Method \"" + key + "\" has an undefined value in the component definition. " +
                        "Did you reference the function correctly?",
                        vm
                    );
                }
                // 如果属性中定义了key,则在methods中不能定义同样的key
                if (props && hasOwn(props, key)) {
                    warn(
                        ("Method \"" + key + "\" has already been defined as a prop."),
                        vm
                    );
                }
                // isReserved 检查一个字符串是否以$或者_开头的字母
                // 事件不能以$或者_开头的字母,因为$ 和 _ 开头的方法一般是内置方法,会重复
                if ((key in vm) && isReserved(key)) { 
                    warn(
                        "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
                        "Avoid defining component methods that start with _ or $."
                    );
                }
            }
            // 把事件放在最外层对象中,如果是函数为空则给一个空函数,如果是有函数则返回函数函数
            vm[key] = methods[key] == null ? noop : bind(methods[key], vm);
        }
}

3-4. initData

先看下initState(vm)此时的入参Vue实例的内容:

$attrs: (...)  // 属性集合
$children: [] // 子节点
$createElement: ƒ (a, b, c, d) //创建节点
$listeners: (...) //属性事件
$options: {components: {}, methods: {}, el: "#app", } //var app=new Vue传进来的data,method等
$parent: undefined
$refs: {}
$root: Vue {_uid: 0, _isVue: true, $options: {}, _renderProxy: Proxy, _self: Vue, }
$scopedSlots: {}
$slots: {}
$vnode: undefined
_c: ƒ (a, b, c, d)
arguments: (...)
caller: (...)
length: 4
name: ""
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: oldVue.js:6695
[[Scopes]]: Scopes[3]
_directInactive: false
_events: {}
_hasHookEvent: false
_inactive: null
_isBeingDestroyed: false
_isDestroyed: false
_isMounted: false
_isVue: true
_renderProxy: Proxy {_uid: 0, _isVue: true, $options: {}, _renderProxy: Proxy, _self: Vue, }
_self: Vue {_uid: 0, _isVue: true, $options: {}, _renderProxy: Proxy, _self: Vue, }
_staticTrees: null
_uid: 0
_vnode: null
_watcher: null
$data: (...)
$isServer: (...)
$props: (...)
$ssrContext: (...)
get $attrs: ƒ reactiveGetter()
set $attrs: ƒ reactiveSetter(newVal)
get $listeners: ƒ reactiveGetter()
set $listeners: ƒ reactiveSetter(newVal)
__proto__: Object

可以看到,此时的vm已经挂载了常用的属性和函数。

作用:

初始化数据 获取options.data 的数据,对data,methods,props进行判重,将data各个值代理到vm上,并 将他们添加到 监听者中。

执行流程:

  • 重置data,如果data是函数,则取data函数的返回,否取自身的值,同时给Vue挂载值为data的_data属性(对象)
  • 经过重置后,data不是一个对象,就重置data为{},并且给出警告
  • 遍历data对象中的key,如果key和methods或者props中的属性名重复了,给出警告
  • 如果key不是 以$或者_开头,代理 data 对象上的属性到 vm 实例
  • 为 data 对象上的数据设置响应式

关联文档:

我们在vue中使用data属性时,直接用this.xxx,即Vue.xxx。那么为什么能直接这么用呢?

就是因为这里做了代理proxy(vm, "_data", key);,将每个函数直接挂载到了vm上了。

源码:

if (opts.data) {
            // 初始化数据 获取options.data 的数据 将他们添加到 监听者中
            initData(vm);
} else {
            //  判断value 是否有__ob__    实例化 dep对象,获取dep对象  为 value添加__ob__ 属性,
            // 把vm._data添加到观察者中  返回 new Observer 实例化的对象
            observe(vm._data = {}, true /* asRootData */);

}

function initData(vm) {
        var data = vm.$options.data;
        // 如果data是函数,则取data函数的返回,否取自身的值,同时给Vue挂载值为data的_data(对象)
        data = vm._data = typeof data === 'function'
            ? getData(data, vm)
            : data || {};

        // 此时拿到的data为: { money: 100, num: 12 }
        if (!isPlainObject(data)) { //如果不是对象 则重置data为{},并发出警告日志
            data = {};
            "development" !== 'production' && warn(
                'data functions should return an object:\n' +
                'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
                vm
            );
        }
        // 例:["money", "num", "arryList", "msgText"]
        var keys = Object.keys(data);
        var props = vm.$options.props; //获取props 属性
        // 例:{ getMoreMoney: ƒ getMoreMoney() }
        var methods = vm.$options.methods; //获取事件
        var i = keys.length; //获取数据key的长度

        while (i--) { //循环data
            var key = keys[i];
            {
                if (methods && hasOwn(methods, key)) { //如果数据中的 key 与事件 中的定义的key 一样 则发出警告
                    warn(
                        ("Method \"" + key + "\" has already been defined as a data property."),
                        vm
                    );
                }
            }

            if (props && hasOwn(props, key)) { //如果数据中的 key 与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)) { //如果key不是 以$或者_开头
                proxy(vm, "_data", key); // 给target即Vue实例上的每个sourceKey上的key设置监听
            }
        }
        // 为 data 对象上的数据设置响应式
        observe(data, true);
}

3-4-1. proxy

作用:

设置代理,将 data和props上的属性 直接代理到 Vue上,设置get和set方法

源码:

proxy(vm, "_props", key);
proxy(vm, "_data", key);

function proxy(
        target,  // Vue实例
        sourceKey, // 监听Vue上的key。 例:sourceKey = '_data'
        key //  例:msgText
    ) {
        debugger
        // 分别挂载get和set方法
        sharedPropertyDefinition.get = function proxyGetter() { //设置get函数
            return this[sourceKey][key] // sourceKey = "_data", key = "num"
        };
        sharedPropertyDefinition.set = function proxySetter(val) {//设置set函数
            this[sourceKey][key] = val;
        };
        // 给target即Vue实例上的每个sourceKey上的key设置监听
        /**
         * target 例:
         * {
                $attrs: (...),
                $children: [],
                $createElement: ƒ (a, b, c, d),
                $listeners: (...),
                ...,
                msgText: "优秀的乃古:",
                get msgText: ƒ proxyGetter(), //代理了这2个方法
                set msgText: ƒ proxySetter(val) //代理了这2个方法
          }
         */
        Object.defineProperty(target, key, sharedPropertyDefinition);
}

var sharedPropertyDefinition = { //共享属性定义
        enumerable: true,
        configurable: true,
        get: noop,
        set: noop
};

执行完以后,会把data中的money和num, arryList直接挂载到Vue实例上去。

image.png

3-5 observe

function initData(vm)最后一步执行了observe(data, true);

作用:

为对象创建观察者实例,如果对象已经被观察过,则返回已有的观察者实例,否则创建新的观察者实例

执行流程:

  • 如果data 不是一个对象 或者 是实例化的VNode,则什么都不做(非对象和 VNode 实例不做响应式处理),退出函数
  • 第一次进来的时候,value中没有__ob__的,如果存在__ob__属性,则表示已经做过观察了,直接返回 __ob__ 属性,ob = value.__ob__
  • 如果没有没有__ob__,如果value是对象和数组,则创建观察者实例,ob = new Observer(value);
  • 如果ob存在,ob上的计数器递增
  • 最后返回ob

源码:

observe(data, true);

function observe(
        value, // 第一次传进来data对象。
               // 例:{ arryList: [{…}],money: 100,msgText: "优秀的乃古:"num: 12 }
        asRootData //asRootData = true
    ) {
        debugger
        //data 不是一个对象 或者 是实例化的VNode
        if (!isObject(value) || value instanceof VNode) {
            return
        }
        var ob;
        // 第一次进来的时候,value中没有__ob__的
        if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
            ob = value.__ob__;
        } else if (
            shouldObserve &&  //shouldObserve 为真
            !isServerRendering() &&  //并且不是在服务器node环境下
            (Array.isArray(value) || isPlainObject(value)) && //是数组或者是对象
            /**
             * Object.isExtensible用于判断对象是否可以被拓展
             */
            Object.isExtensible(value) &&
            !value._isVue //_isVue为假
        ) {
            //实例化 dep对象 为 value添加__ob__ 属性
            ob = new Observer(value);
        }
        // 如果是RootData,即咱们在新建Vue实例时,传到data里的值,只有RootData在每次observe的时候,会进行计数。 
        // vmCount是用来记录此Vue实例被使用的次数的, 比如,我们有一个组件logo,页面头部和尾部都需要展示logo,
        // 都用了这个组件,那么这个时候vmCount就会计数,值为2
        if (asRootData && ob) { //是根节点数据的话 并且 ob 存在
            ob.vmCount++; //统计有几个vm
        }
        // 实例化 dep对象,获取dep对象  为 value添加__ob__ 属性
        //例: { dep: Dep {id: 7, subs: Array(0)}, value: {__ob__: Observer}, vmCount: 0 }
        return ob 
}

3-6 Observer

作用:

Observer构造函数,为observe服务的。

观察者类,会被附加到每个被观察的对象上, 为 value添加__ob__ 属性, 而对象的各个属性则会被转换成 getter/setter,并收集依赖和通知更新

执行流程:

  • 将当前Observer挂载到data的__ob__上
  • 如果value是数组,则遍历数组的每一项,执行observe观察
  • 如果是对象,遍历每个属性并将其转换为getter / setter

源码:

ob = new Observer(value);

var Observer = function Observer(value) {
        this.value = value;
        this.dep = new Dep();
        this.vmCount = 0;
        //设置监听 value 必须是对象
        debugger
        // 给value添加__ob__属性,值就是本Observer对象,value.__ob__ = this;
        // Vue.$data 中每个对象都有 __ob__ 属性,包括 Vue.$data对象本身
        def(value, '__ob__', this);
        debugger
        /** 
        * value 为数组 
        * hasProto = '__proto__' in {} 
        * 用于判断对象是否存在 __proto__ 属性,
          通过 obj.__proto__ 可以访问对象的原型链 
        * 但由于 __proto__ 不是标准属性,所以有些浏览器不支
          持,比如 IE6-10,Opera10.1 
        * 为什么要判断,是因为一会儿要通过 __proto__ 操作数据的原型链
          覆盖数组默认的七个原型方法,以实现数组响应式 
        */
        if (Array.isArray(value)) { //判断是不是数组
            var augment = hasProto  //__proto__ 存在么 高级浏览器都会有这个
                ? protoAugment
                : copyAugment;
            augment(value, arrayMethods, arrayKeys);
            this.observeArray(value); //遍历value,执行 observe(items);最终还是会执行 walk 方法
        } else {
            // 为对象的每个属性(包括嵌套对象)设置响应式
            this.walk(value);
        }
};

// 当前浏览器支持`__proto__`时,将数组的api都挂载到value的原型上
function protoAugment(
    target, // 就是上面的value(当为数组格式时)
    src,  // 数组api集合
    keys //api的key的集合 这里不会用上
    ) {
        target.__proto__ = src;
}

// 当前浏览器不支持`__proto__`时,给value设置代理,挂载数组方法
function copyAugment(
    target, // 就是上面的value(当为数组格式时)
    src,  // 数组api集合
    keys //api的key的集合
    ) {
        for (var i = 0, l = keys.length; i < l; i++) {
            var key = keys[i];
            def(target, key, src[key]);
        }
}


3-6-1 def

作用:

用defineProperty 定义属性,给传入的对象上的key重写为传入的函数

源码:

  /**用defineProperty 定义属性
     第一个参数是对象
     第二个是key
     第三个是函数
     第四个是 是否可以枚举。
     */
    function def(
        obj, // obj = Array {} ,数组原型对象
        key, // key = "push
        val, // val = ƒ mutator()
        enumerable // enumerable = undefined
        ) {
        // obj即传入的数组的原型方法,这里如果传的key为push,则是给数组原型挂载了传入的方法ƒ mutator()
        // 如: obj = { push: ƒ mutator(), __proto__: Array(0) }
        Object.defineProperty(obj, key, { 
            value: val, //值
            enumerable: !!enumerable,  //定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。
            writable: true, //可以 改写 value
            configurable: true  //configurable特性表示对象的属性是否可以被删除,以及除writable特性
            //外的其他特性是否可以被修改。
        });
    }

def函数的另外一个作用:

def函数在多个地方有用到,我们知道Vue能对数组响应式,因为对数组方法进行了重写,其代码如下:

    var arrayProto = Array.prototype;
    var arrayMethods = Object.create(arrayProto);

    // 重写了数组的这些方法
    var methodsToPatch = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
    ];

    /**
     * 更新数据时候如果是数组拦截方法,如果在数据中更新用的
       是'push','pop','shift','unshift','splice','sort','reverse' 方法则会调用这里
    */
    methodsToPatch.forEach(function (
        method //数组的方法名
        ) {
        // 获取数组的各个原生方法
        var original = arrayProto[method];
        debugger
        def(arrayMethods, method, function mutator() {
            var args = [], len = arguments.length;
            while (len--) args[len] = arguments[len];
            var result = original.apply(this, args);
            var ob = this.__ob__;
            var inserted;
            switch (method) {
                case 'push':
                case 'unshift':
                    inserted = args;
                    break
                case 'splice':
                    inserted = args.slice(2);
                    break
            }
            if (inserted) {
                //观察数组数据
                ob.observeArray(inserted);
            }
            //更新通知
            ob.dep.notify();
            return result
        });
    });
    
   /**依次给传的数组原型对象上的方法设置重写,重写为传入的函数
     用defineProperty 定义属性
     第一个参数是对象
     第二个是key
     第三个是vue
     第四个是 是否可以枚举。
     */
    function def(
        obj, // obj = Array {} ,数组原型对象
        key, // key = "push
        val, // val = ƒ mutator()
        enumerable // enumerable = undefined
        ) {
            debugger
        // obj即传入的数组的原型方法,这里如果传的key为push,则是给数组原型挂载了传入的方法ƒ mutator()
        // 如: obj = { push: ƒ mutator(), __proto__: Array(0) }
        Object.defineProperty(obj, key, { 
            value: val, //值
            enumerable: !!enumerable,  //定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。
            writable: true, //可以 改写 value
            configurable: true  //configurable特性表示对象的属性是否可以被删除,以及除writable特性外的其他特性是否可以被修改。
        });
    }

3-6-2 walk

作用:

遍历每个属性并将它们转换为getter/setter。

源码:

    Observer.prototype.walk = function walk(obj) {
        debugger
        var keys = Object.keys(obj); //此时 keys = ["money", "num", "arryList"]
        for (var i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i]); // 传入对象各个key
        }
    };

我们看下defineReactive函数,它非常的长,我们将在【章节4】进行讲解。

defineReactive的作用是:

  • 在object上定义一个响应式的属性
  • 把对象obj里的属性key变成一个getter/setter形式的响应式的属性
  • 同时在getter的时候收集依赖,并在setter的时候触发依赖。

3-6-3 总结

observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,给每个属性加上set和get方法,以此来达到实现侦测对象变化。值得注意的是,observe 会进行递归调用。

observe执行完后返回的ob为:

{
    dep: {id: 2, subs: Array(0)},
    value: {
        "money": 100,
        "num": 12,
        "arryList": [
            {
                "name": "子树"
            }
        ]
    },
    vmCount: 1,
    __proto__: Object
}

再回到initData函数observe(data, true),

这时的data已经变成下面这样的格式了:

{
    arryList: [{ name: "子树" }],
    money: 100,
    num: 12,
    __ob__: Observer {value: {…}, dep: Dep, vmCount: 1},
    get arryList: ƒ reactiveGetter(),
    set arryList: ƒ reactiveSetter(newVal),
    get money: ƒ reactiveGetter(),
    set money: ƒ reactiveSetter(newVal),
    get num: ƒ reactiveGetter(),
    set num: ƒ reactiveSetter(newVal),
    __proto__: Object
}

每个属性都已经挂载了相应的get和set方法。

3-7. initComputed

作用:

执行流程:

  • 传入计算属性集合
  • 创建一个新的监听者对象watchers,同时挂载到vm的_computedWatchers上(由于是引用类型,后续修改watchers就意味着修改vm的_computedWatche)
  • 定义getter, 如果计算属性值为函数,则取它本身,否则取它的get属性
  • 如果getter是空 警告: 计算属性缺少Getter
  • 如果不是node ssr渲染,给watchers添加计算属性的观察者
  • 判断computed 的属性key
    • 如果不在vm中,定义计算属性 并且 把计算属性的key和计算函数 直接添加到vm上(所以我们能直接用this.propsxx)
    • 如果在vm中,而且在$data或者props中,警告:计算属性在data或者prop已经定义,不能重复重复定义 源码:
if (opts.computed) {
       initComputed(vm, opts.computed);
}
        
function initComputed(
        vm, 
        computed // 例:{ total: ƒ total() }
        ) {
        debugger
        //创建一个新的监听者对象空对象,同时挂载到vm的_computedWatchers上
        var watchers = vm._computedWatchers = Object.create(null);
        // 服务器呈现  判断是不是node 服务器环境
        var isSSR = isServerRendering(); 

        for (var key in computed) {
            var userDef = computed[key]; // 获取计算函数
            // 定义getter, 如果计算属性值为函数,则取它本身,否则取它的get属性
            var getter = typeof userDef === 'function' ? userDef : userDef.get; 
            //如果getter是空 警告 计算属性缺少Getter
            if ("development" !== 'production' && getter == null) { 
                warn(
                    ("Getter is missing for computed property \"" + key + "\"."),
                    vm
                );
            }
            // 如果不是node ssr渲染,给watchers添加计算属性的观察者
            // 这一步可以看出computed也是一个watcher
            if (!isSSR) { 
                watchers[key] = new Watcher(
                    vm, //vm  vode
                    getter || noop,  //noop为空函数
                    noop,  // 回调函数
                    computedWatcherOptions  //参数 lazy = true
                );
                /**
                 * watchers,例:
                 * { 
                     total:{ 
                         vm: Vue, deep: false, user: false, lazy: true, sync: false, …
                        } 
                    }
                 */

            }
            if (!(key in vm)) { 
                // 如果computed 属性key,不在vm中,
                // 定义计算属性 并且 把计算属性的key和计算函数 添加到vm上
                defineComputed(vm, key, userDef); 
            } else {
                // 警告:计算属性在data或者prop已经定义,不能重复重复定义
                if (key in vm.$data) {
                    warn(("The computed property \"" + key + "\" is already defined in data."), vm);
                } else if (vm.$options.props && key in vm.$options.props) {
                    warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
                }
            }
        }
}

3-7-1. defineComputed

作用:

定义计算属性 并且 把属性的数据 添加到对象监听中

源码:

function initComputed(vm, computed) {
        for (var key in computed) {
            var userDef = computed[key]; // 获取计算函数
            if (!(key in vm)) {
                // 如果computed 属性key,不在vm中,
                // 定义计算属性 并且 把计算属性的key和计算函数 添加到vm上
                defineComputed(vm, key, userDef);
            }
        }
}
    
function defineComputed(
        target, //目标
        key, //key
        userDef //值
    ) {
        var shouldCache = !isServerRendering(); //如果不是node服务器 是浏览器
        if (typeof userDef === 'function') { //属性的值如果是个函数
            sharedPropertyDefinition.get = shouldCache
                ? createComputedGetter(key) //如果不是node服务器 是浏览器    创建计算属性 获取值 收集 dep 依赖
                : userDef; //node 服务器取值 直接调用该函数
            sharedPropertyDefinition.set = noop; //赋值一个空函数
        } else {
            sharedPropertyDefinition.get = userDef.get ?//如果userDef.get 存在
                (shouldCache && userDef.cache !== false ? //缓存
                    createComputedGetter(key) :  //创建计算属性 获取值 收集 dep 依赖
                    userDef.get
                ) :
                noop;  //如果userDef.get 不存在给一个空的函数
            sharedPropertyDefinition.set = userDef.set //如果userDef.set 存在
                ? userDef.set
                : noop;
        }
        if ("development" !== 'production' &&
            sharedPropertyDefinition.set === noop) { //如果设置值等于一个空函数则警告
            sharedPropertyDefinition.set = function () {
                warn(
                    ("Computed property \"" + key + "\" was assigned to but it has no setter."),
                    this
                );
            };
        }
        //添加对象监听
        Object.defineProperty(target, key, sharedPropertyDefinition);
}

var sharedPropertyDefinition = { //共享属性定义
        enumerable: true,
        configurable: true,
        get: noop,
        set: noop
};


3-7-2. createComputedGetter

作用:

创建计算属性 获取值 收集 dep 依赖

源码:

function createComputedGetter(key) {
        // 用户取值的时候会调用此方法
        return function computedGetter() {
            // Watcher 实例化之后的对象
            var watcher = this._computedWatchers && this._computedWatchers[key];
            if (watcher) {
                if (watcher.dirty) {
                    //dirty为true会去进行求值,这儿的dirty起到了缓存的作用
                    //this.value 获取值 this.getter
                    watcher.evaluate(); 
                }
                if (Dep.target) {
                    //为Watcher 添加 为Watcher.newDeps.push(dep); 一个dep对象
                    //循环deps 收集 newDeps dep 当newDeps 数据被清空的时候重新收集依赖
                    watcher.depend();
                }
                //返回值
                return watcher.value
            }
        }
}
    
Watcher.prototype.evaluate = function evaluate() {
   this.value = this.get(); //获取值
   this.dirty = false; // 懒惰者标志  标志已经获取过一次值
};

3-8. initWatch

作用:

遍历watch,如果属性值是数组,则遍历属性值(watch配置项),创建watch

源码:

if (opts.watch && opts.watch !== nativeWatch) {
     //初始化Watch
     initWatch(vm, opts.watch);
}

//初始化Watch监听
function initWatch(
        vm, 
        watch // 例:{ money: {deep: true, immediate: true, handler: ƒ} }
        ) {
        debugger
        //循环watch对象
        for (var key in watch) {
            var handler = watch[key]; //获取单个watch配置项
            //如果他是数组handler
            if (Array.isArray(handler)) {
                //循环数组 创建 监听
                for (var i = 0; i < handler.length; i++) {
                    createWatcher(
                        vm, //vm 是 vue对象
                        key, //key
                        handler[i]//函数或者对象
                    );
                }
            } else {
                //循环数组 创建 监听
                createWatcher(
                    vm, // vm 是 vue对象
                    key, // 例:"money"
                    handler // 例:{deep: true, immediate: true, handler: ƒ}
                );
            }
        }
}

3-8-1. createWatcher

作用:

遍历watch,如果属性值是数组,则遍历属性值(watch配置项),创建watch

执行流程:

  • watch的配置项handler如果是个对象(常用格式下),获取配置项的执行函数handler(注意区别前面的hangler)
  • watch的配置项handler如果是字符串,直接在vm获取对应执行函数,
  • 最后返回vm.$watch,兼容处理cb,根据immediate来是否立即执行watch函数,卸载观察者,解除监听

源码:

// 转义handler 并且为数据 创建 Watcher 观察者
function createWatcher(
        vm,  // vm对象
        expOrFn, // 例:"money"
        handler, // 例:vue组件中watch中money对应的对象,{deep: true, immediate: true, handler: ƒ}
        options  // 参数
    ) {
        if (isPlainObject(handler)) {  //判断是否是对象
            options = handler;
            handler = handler.handler; // handler.handler是函数
        }
        if (typeof handler === 'string') { //判断handler 是否是字符串 如果是 则是key
            handler = vm[handler]; //取值 vm 就是Vue 最外层 中的函数
        }
        //转义handler 并且为数据 创建 Watcher 观察者
        return vm.$watch(
            expOrFn,// key 值 或者函数
            handler, //函数
            options //参数
        )
        /**
         * vm.$watch执行后,会推入到vm.$watchs数组里面,例:
         * [{
                active: true,
                cb: ƒ handler(newVal, oldVal),
                deep: true,
                depIds: Set(1) {3},
                deps: [Dep],
                dirty: false,
                expression: "money",
                getter: ƒ (obj),
                id: 2,
                lazy: false,
                newDepIds: Set(0) {},
                newDeps: [],
                sync: false,
                user: true,
                value: 100,
                vm: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
         * }]
         */
}

3-8-2. $watch

作用:

兼容处理cb,根据immediate来是否立即执行watch函数,卸载观察者,解除监听

执行流程:

  • 如果cb是一个对象,需要递归执行createWatcher(它又返回vm.$watch)
  • 设置user标志为true,用户手动监听, 就是在 options 自定义的 watch
  • 创建一个新的watcher
  • 如果immediate属性值为true,立即回调触发函数
  • 最后执行watcher.teardown(),卸载监听

关联文档:

我们写watch的时候,如果设置了 immediate:true,则watch一开始就会触发,那是因为下面的这段代码:if (options.immediate) { cb.call(vm, watcher.value); }

源码:

// new Vue之前执行
function stateMixin(Vue) {
    ...
    Vue.prototype.$watch = function (
            expOrFn, //watch的name,例:"money"  
            cb, // watch的执行函数,例:ƒ handler(newVal, oldVal)
            options //watch的参数:例:{deep: true, immediate: true, handler: ƒ}
        ) {
            debugger
            var vm = this;
            //判断是否是对象 如果是对象则递归 深层 监听 直到它不是一个对象的时候才会跳出递归
            if (isPlainObject(cb)) { 
                //  转义handler 并且为数据 创建 Watcher 观察者
                return createWatcher(
                    vm,
                    expOrFn,
                    cb,
                    options
                )
            }
            options = options || {};
            options.user = true; //用户手动监听, 就是在 options 自定义的 watch
            var watcher = new Watcher(
                vm, 
                expOrFn, 
                cb,
                options 
            );
             /**
             * watch,例:
                {
                    active: true,
                    cb: ƒ handler(newVal, oldVal),
                    deep: true,
                    depIds: Set(1) {3},
                    deps: [Dep],
                    dirty: false,
                    expression: "money",
                    getter: ƒ (obj),
                    id: 2,
                    lazy: false,
                    newDepIds: Set(0) {},
                    newDeps: [],
                    sync: false,
                    user: true,
                    value: 100,
                    vm: Vue {_uid: 0, _isVue: true, $options: {…},...},
                    __proto__: Object
                }
             */
            // immediate属性值为true,立即回调触发函数
            if (options.immediate) {
                cb.call(vm, watcher.value);
            }
            return function unwatchFn() { //卸载观察者,解除监听
                //从所有依赖项的订阅方列表中删除self。
                watcher.teardown();
            }
        };
 }

3-8-3. Watcher

作用:

Watcher构造函数

执行流程:

源码:

    var Watcher = function Watcher(
        vm, //vm dom
        expOrFn,  //获取值的函数,或者是更新viwe试图函数
        cb, //回调函数,回调值给回调函数
        options, //参数
        isRenderWatcher//是否渲染过得观察者
    ) {
        this.vm = vm;
        //是否是已经渲染过得观察者
        if (isRenderWatcher) { //把当前 Watcher 对象赋值给 vm._watcher上
            vm._watcher = this;
        }
        //把观察者添加到队列里面 当前Watcher添加到vue实例上
        vm._watchers.push(this);
        // options
        if (options) { //如果有参数
            this.deep = !!options.deep; //实际
            this.user = !!options.user; //用户
            this.lazy = !!options.lazy; //懒惰 ssr 渲染
            this.sync = !!options.sync; //如果是同步
        } else {

            this.deep = this.user = this.lazy = this.sync = false;
        }
        this.cb = cb; //回调函数
        this.id = ++uid$1; // uid for batching uid为批处理  监听者id
        this.active = true; //激活
        this.dirty = this.lazy; // for lazy watchers 对于懒惰的观察者
        this.deps = [];    // 观察者队列
        this.newDeps = []; // 新的观察者队列
        // 内容不可重复的数组对象
        this.depIds = new _Set();
        this.newDepIds = new _Set();
        // 把函数变成字符串形式
        this.expression = expOrFn.toString();
        //getter的解析表达式
        if (typeof expOrFn === 'function') {
            //获取值的函数
            this.getter = expOrFn;
        } else {
            //如果是keepAlive 组件则会走这里
            //path 因该是路由地址
            // if (bailRE.test(path)) {  //  匹配上 返回 true     var bailRE = /[^\w.$]/;  //匹配不是 数字字母下划线 $符号   开头的为true
            //     return
            // }

            // //匹配不上  path在已点分割
            // var segments = path.split('.');
            // return function (obj) {
            //
            //     for (var i = 0; i < segments.length; i++) {
            //         //如果有参数则返回真
            //         if (!obj) {
            //             return
            //         }
            //         //将对象中的一个key值 赋值给该对象 相当于 segments 以点拆分的数组做obj 的key
            //         obj = obj[segments[i]];
            //     }
            //     //否则返回一个对象
            //     return obj
            // }

            //匹配不是 数字字母下划线 $符号   开头的为true

            this.getter = parsePath(expOrFn);
            if (!this.getter) { //如果不存在 则给一个空的数组
                this.getter = function () {
                };
                "development" !== 'production' && warn(
                    "Failed watching path: \"" + expOrFn + "\" " +
                    'Watcher only accepts simple dot-delimited paths. ' +
                    'For full control, use a function instead.',
                    vm
                );
            }
        }
        this.value = this.lazy ?  //   lazy为真的的时候才能获取值  这个有是组件才为真
            undefined :
            this.get(); //计算getter,并重新收集依赖项。 获取值
    };

3-8-4. Watcher.prototype.get

作用:

计算getter,并重新收集依赖项。 获取value值

执行流程:

源码:

    Watcher.prototype.get = function get() {
        //添加一个dep target
        pushTarget(this);
        var value;
        var vm = this.vm;
        try {
            //获取值 如果报错 则执行catch
            value = this.getter.call(vm, vm);
        } catch (e) {
            if (this.user) {
                handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
            } else {
                throw e
            }
        } finally {

            // "touch" every property so they are all tracked as
            // dependencies for deep watching
            //“触摸”每个属性,以便它们都被跟踪为
            //依赖深度观察
            if (this.deep) {

                // //如果val 有__ob__ 属性
                // if (val.__ob__) {
                //     var depId = val.__ob__.dep.id;
                //     // seen 中是否含有depId 属性或者方法
                //     if (seen.has(depId)) {
                //         return
                //     }
                //     //如果没有则添加进去
                //     seen.add(depId);
                // }
                //为 seenObjects 深度收集val 中的key
                traverse(value);
            }
            // 出盏一个pushTarget
            popTarget();
            //清理依赖项集合。
            this.cleanupDeps();
        }
        //返回值
        return value
    };

3-8-5. traverse

源码:

    function traverse(val) {
        // 搜索seen 为seen添加depId
        //seenObjects set对象
        //  为 seenObjects 深度收集val 中的key

        _traverse(val, seenObjects);
        //清除对象 给对象置空
        seenObjects.clear();
    }
    
        function _traverse(val, seen) {
        // de_traverse
        var i, keys;
        //判断是否是数组
        var isA = Array.isArray(val);
        //isFrozen 方法判断一个对象是否被冻结。
        //val 是否是被VNode 实例化
        if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
            return
        }
        //如果val 有__ob__ 属性
        if (val.__ob__) {
            var depId = val.__ob__.dep.id;
            // seen 中是否含有depId 属性或者方法
            if (seen.has(depId)) {
                return
            }
            // seen 是 seenObjects = new _Set(); add 就是set对象中的add方法,添加为一的值得key
            //如果没有则添加进去
            seen.add(depId);
        }
        //如果是数组
        if (isA) {
            i = val.length;
            //则循环检查 回调递归
            while (i--) {
                _traverse(val[i], seen);
            }
        } else {

            keys = Object.keys(val);
            i = keys.length;
            //如果是对象也循环递归检查
            while (i--) {
                _traverse(val[keys[i]], seen);
            }
        }
    }

4. defineReactive

作用: 在object上定义一个响应式的属性,这个方法,就是把对象obj里的属性key变成一个getter/setter形式的响应式的属性 同时在getter的时候收集依赖,并在setter的时候触发依赖。

执行流程:

  • 初始化了一个依赖的对象dep(例:{id: 3, subs: Array(0)}),对象中有空的观察者列表,用来把用到该属性的依赖都放在这里面。
  • 如果传入传入对象的对应的key不能修改,就退出函数
  • 如果该属性没有get或者有set,而且只传了2个参数,那就设置传入的val为传入的属性值
  • 判断value 是否有__ob__ 实例化 dep对象,获取dep对象 为 value添加__ob__ 属性
  • 通过Object.defineProperty设置属性变成了getter和setter的形式

源码:

Observer.prototype.walk = function walk(obj) {
        var keys = Object.keys(obj);
        for (var i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i]);
        }
};
    
    function defineReactive(
        obj, //对象 obj = {money: 100, num: 12, arryList: Array(1), __ob__: Observer}
        key,//对象的key key = "money"
        val, //监听的数据 返回的数据 val = undefined
        customSetter, //  日志函数 shallow = undefined
        shallow //是否要添加__ob__ 属性  shallow = undefined
    ) {
        
        //初始化了一个依赖的对象,对象中有空的观察者列表,用来把用到该属性的依赖都放在这里面。
        var dep = new Dep(); // dep = Dep {id: 3, subs: Array(0)}
        /**
         * getOwnPropertyDescriptor方法:
         * 判断该属性key是否是自有属性,obj为传入的对象,例如:
         * Object.getOwnPropertyDescriptor(obj, 'money'),结果如下:
         * {value: 100, writable: true, enumerable: true, configurable: true}
         * 通过configurable判断该属性能否修改,不能修改就不需要做响应式处理,
         * 直接退出函数
         */
        var property = Object.getOwnPropertyDescriptor(obj, key);
        if (property && property.configurable === false) {
            return
        }

        // 获取已经实现的 getter /setter 方法
        var getter = property && property.get;
        var setter = property && property.set;
        
        // 如果该属性没有get或者有set,而且只传了2个参数,那就设置val为传入的属性值
        // arguments即defineReactive的入参,由例子可以看到,此时其它值为undefined
        // 只有2个参数是有值的,因此arguments为2
        if ((!getter || setter) && arguments.length === 2) {
            val = obj[key]
        }
        debugger
        /**
         * 判断value 是否有__ob__    实例化 dep对象,获取dep对象  为 value添加__ob__ 属性
         * 递归把val添加到观察者中  返回 new Observer 实例化的对象
         */
        var childOb = !shallow && observe(val);
        debugger
        console.log(Dep.target)
        //属性变成了getter和setter的形式
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function reactiveGetter() {
                // 该属性的值返回去,同时收集依赖,记录哪些地方用到了该属性。
                /**
                 * getter即上面的property.get
                 * 判断属性中是否有get方法(如通过getOwnPropertyDescriptor判断'money'是否有get方法)
                 * 如有,则调用这个get方法
                 * 如果没有,直接取obj上对应的key的值
                 */
                // 
                var value = getter ? getter.call(obj) : val;
                //Dep.target 全局变量指向的就是当前正在解析指令的Complie生成的 Watcher
                // 会执行到 dep.addSub(Dep.target), 将 Watcher 添加到 Dep 对象的 Watcher 列表中
                if (Dep.target) {  //Dep.target 静态标志 标志了Dep添加了Watcher 实例化的对象
                    //添加一个dep
                    dep.depend(); // 实际执行function depend(),然后执行function addDep(dep)
 
                    if (childOb) {  //如果子节点存在也添加一个dep
                        childOb.dep.depend();
                        if (Array.isArray(value)) {  //判断是否是数组 如果是数组
                            dependArray(value);   //则数组也添加dep
                        }
                    }
                }
                return value
            },
            set: function reactiveSetter(newVal) {
                /**
                 * 新的值newVal,赋值给旧的val,如果有以前的setter,就用以前的setter,如果没有setter,就返回。
                 * 最后新的值添加__ob__属性,然后触发该属性的依赖,通知他们去变更
                 */
                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) {
                    //set 方法 设置新的值
                    setter.call(obj, newVal);
                } else {
                    //新的值直接给他
                    val = newVal;
                }

                //observe 添加 观察者
                childOb = !shallow && observe(newVal);
                // 如果数据被重新赋值了, 调用 Dep 的 notify 方法, 通知所有的 Watcher
                dep.notify();
            }
        });
    }

实际上,在new Vue中执行初始化过程中,就已经执行了几次defineReactive了,如:initRender,defineReactive

defineReactive(
                vm, // Vue实例
                '$attrs',
                parentData && parentData.attrs || emptyObject,  // {}
                function () {
                    !isUpdatingChildComponent && warn("$attrs is readonly.", vm);
                },
                true                
);
defineReactive(
                vm, '$listeners', 
                options._parentListeners || emptyObject, 
                function () {
                    !isUpdatingChildComponent && warn("$listeners is readonly.", vm);
                }, 
                true
);

它给Vue上挂载了$attrs,$listeners等属性。

我们分析下从observe走到defineReactive的流程:

observe()=>new Observer=>function Observer()=>this.walk()=> defineReactive(obj, keys[i])=>function defineReactive()

此时,执行defineReactive传入的参数为

    function defineReactive(
        obj, //对象 obj = {money: 100, num: 12, arryList: Array(1), __ob__: Observer}
        key,//对象的key key = "money"
        val, //监听的数据 返回的数据 val = undefined
        customSetter, //  日志函数 shallow = undefined
        shallow //是否要添加__ob__ 属性  shallow = undefined
    )

可以看到shallow的值为空,所以执行到var childOb = !shallow && observe(val);,就会取后面observe(val)去执行。它又走进observe函数了,这个函数在上一节有说明。注意,面执行了val = obj[key],已经将val重置为100了。

function observe(
        value, // 100
        asRootData // undefined
        ){
        // value 不是一个对象 或者 是实例化的VNode
        if (!isObject(value) || value instanceof VNode) {
            return
        }
}

此时value不满足条件,直接退出,childOb还是undefined。

我们再回到defineReactive函数中,他会给传入的object对应的key设置get和set方法,属性变成了getter和setter的形式。

4-1. Dep

作用: 一个 dep 对应一个 obj.key

在读取响应式数据时,负责收集依赖,每个 dep(或者说 obj.key)依赖的 watcher 有哪些

在响应式数据更新时,负责通知 dep 中那些 watcher 去执行 update 方法

执行流程:

  • Dep默认有id(确保每个Dep都有唯一的ID)和subs(用来存放Watcher对象的数组)
  • 并向原型挂载了几个方法:
    • addSub:向subs数组添加依赖Watcher
    • removeSub:移除依赖
    • depend:设置某个Watcher的依赖
    • notify:通知所有Watcher对象更新视图
    • addSub:向subs数组添加依赖Watcher

源码:

    var uid = 0;

    // Dep是订阅者Watcher对应的数据依赖,它将watch全部装入自身的subs数组中
    var Dep = function Dep() {
        //每个Dep都有唯一的ID
        this.id = uid++;
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    };
    
    //向subs数组添加依赖Watcher
    Dep.prototype.addSub = function addSub(sub) {
        this.subs.push(sub);
    };
    
    //移除依赖
    Dep.prototype.removeSub = function removeSub(sub) {
        remove(this.subs, sub);
    };
    
    //设置某个Watcher的依赖
    //这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
    //也就是说判断他是Watcher的this.get调用的,而不是普通调用
    Dep.prototype.depend = function depend() {
        //添加一个dep    target 是Watcher dep就是dep对象
        if (Dep.target) {
            //像指令添加依赖项
            Dep.target.addDep(this);
        }
    };
    /* 通知所有Watcher对象更新视图 */
    Dep.prototype.notify = function notify() {
        var subs = this.subs.slice();
        for (var i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    };

    Dep.target = null;
    // 目标堆栈
    var targetStack = [];

    // 将dep存在的观察者放入targetStack统一管理,然后更新Dep.target为传入的观察者
    function pushTarget(_target) {
        //target 是Watcher dep就是dep对象
        if (Dep.target) {
            targetStack.push(Dep.target);
        }
        Dep.target = _target;
    }

4-2. queueWatcher

将观察者推进 queue 队列中 过滤重复的 id 除非是*刷新队列时推送

    Watcher.prototype.update = function update() {
        // Watcher的lazy默认是false
        if (this.lazy) {
            this.dirty = true; // 懒惰者标志
        } else if (this.sync) { //如果是同步
            //更新数据
            this.run();
        } else {
            //如果是多个观察者
            queueWatcher(this); //队列中的观察者
        }
    };
    function queueWatcher(watcher) {
        debugger
        var id = watcher.id;
        //has是记录观察者的id的对象 例:{ 2: true }
        // 如果传进来的watcher的id在has中不存在,就需要在has上加上
        // 如果已经存在了,就说明当前的watch是重复的,直接退出函数
        if (has[id] == null) {
            has[id] = true;
            // 默认是false,进入flushSchedulerQueue 函数等待标志 
            if (!flushing) {
                queue.push(watcher); //把观察者添加到队列中,queue为记录观察者队列的数组
            } else {
                // 如果已经刷新,则根据监视程序的id拼接它
                // 如果已经通过了它的id,那么将立即运行next。
                var i = queue.length - 1;
                while (i > index && queue[i].id > watcher.id) {
                    i--;
                }
                //根据id大小拼接插入在数组的哪个位置
                queue.splice(i + 1, 0, watcher);
            }
            // queue the flush
            if (!waiting) {
                waiting = true;
                //为callbacks 收集队列cb 函数 并且根据 pending 状态是否要触发callbacks 队列函数
                debugger
                nextTick(
                    flushSchedulerQueue//更新观察者 运行观察者watcher.run() 函数 并且   调用组件更新和激活的钩子
                );
            }
        }
    }

4-3. nextTick

为callbacks 收集队列cb 函数 并且根据 pending 状态是否要触发callbacks 队列函数

    function nextTick(cb, ctx) {
        debugger
        //cb 回调函数
        //ctx this的指向
        var _resolve;
        //添加一个回调函数到队列里面去
        callbacks.push(function () {
            if (cb) {
                //如果cb存在 并且是一个函数就执行
                try {
                    cb.call(ctx);
                } catch (e) {
                    //如果不是函数则报错
                    handleError(e, ctx, 'nextTick');
                }
            } else if (_resolve) {
                //_resolve 如果存在则执行
                _resolve(ctx);
            }
        });
        if (!pending) {
            pending = true;
            //执行异步宏任务
            if (useMacroTask) {
                macroTimerFunc(); //异步触发 或者 实现观察者 触发  callbacks 队列中的函数
            } else {
                microTimerFunc(); //异步触发 或者 实现观察者 触发  callbacks 队列中的函数
            }
        }
        if (!cb && typeof Promise !== 'undefined') {
            //如果回调函数不存在 则声明一个Promise 函数
            return new Promise(function (resolve) {
                _resolve = resolve;
            })
        }
    }

4-4. macroTimerFunc

在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate。 而常见的 micro task 有 MutationObsever 和 Promise.then。 Vue中对于 macro task 的实现:

  • 优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性
  • 不支持的话再去检测是否支持原生的MessageChannel,
  • 如果也不支持的话就会降级为 setTimeout 0。
    var microTimerFunc; //微计时器功能
    var macroTimerFunc; //宏计时器功能
    var useMacroTask = false; //使用宏任务
    if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
        //函数表达式赋值给macroTimerFunc
        macroTimerFunc = function () {
            debugger
            setImmediate(flushCallbacks);
        };
    } else if (typeof MessageChannel !== 'undefined' && (
        isNative(MessageChannel) ||
        // PhantomJS
        MessageChannel.toString() === '[object MessageChannelConstructor]'
    )) {
       /**
        * 创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,
        * 而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据。
        */
        var channel = new MessageChannel();
        var port = channel.port2;
        //设置端口1 的接受函数为flushCallbacks
        channel.port1.onmessage = flushCallbacks;

        //端口2推送信息给端口1,发送的内容为1
        macroTimerFunc = function () {
            debugger
            port.postMessage(1);
        };
    } else {
        macroTimerFunc = function () {
            debugger
            setTimeout(flushCallbacks, 0);
        };
    }

源码提问

1. 请说一下响应式数据的原理

vue2——核心点:Object.defineProperty —— 修改每一个属性

  • 默认Vue在初始化数据时,会给data中的属性使用Object.defineProperty,在获取和设置的进行拦截,重新定义所有属性。
  • 当页面取到对应属性时,会进行依赖收集(收集当前组件的watcher)。
  • 如果属性发生变化会通知相关依赖进行更新操作。

依赖收集、派发更新的作用:

如果没有这项操作,每个数据更新就会去渲染页面,极大的消耗性能。加了这项操作,去监听相关数据的改变,添加到队列里,当所有改变完事儿之后,一起进行渲染。

vue3——核心点:proxy(代理)—— 直接处理对象

解决了vue2中的处理对象递归、处理数组麻烦的问题

原理:

image.png

响应式原理图

image.png

2.vue中是如何检测数组变化的?

使用了函数劫持的方式,重写了数组方法。

Vue将data中的数组,进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次进行监控。

  1. Object.create(Array.prototype) 复制 Array 原型链为新的对象;
  2. 拦截了数组的 7 个方法的执行,并使其可响应,7 个方法分别为:pushpopshiftunshiftsplicesortreverse
  3. 当数组调用到这 7 个方法的时候,执行 ob.dep.notify() 进行派发通知 Watcher 更新;

原理:

image.png

具体请看【3-6-1 def】

3.为何vue采用异步渲染?

vue是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件重新渲染。为了性能考虑,vue会在本轮数据更新后,再去异步更新视图。

原理:

image.png

blog.csdn.net/weixin_4097…

4. nextTick实现原理?

nextTick主要是使用了宏任务和微任务,定义了一个异步方法。多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列,所以nextTick就是异步方法。

image.png

5. vue中computed的特点

默认computed也是一个watcher,具备缓存,只有当依赖的属性发生变化才会更新视图。

image.png

6. watch中的deep:true是如何实现的?

当用户指定了watch中的deep属性为true时,如果当时监控的属性是数组类型,会对对象中的每一项进行求值,此时会将当前watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新。 内部原理就是递归,耗费性能

  • initWatch
  • createWatcher
  • Vue.prototype.$watch
  • new Watcher: this.get();
  • Watcher.prototype.get:if (this.deep) {//深度监听 traverse(value) }
  • traverse

7. 组件中的data为什么是个函数?

同一个组件被复用多次,会创建多个实例。
这些实例用的是同一个构造函数,如果data是一个对象的话,所有组件共享了同一个对象。
为了保证组件的数据独立性,要求每个组件都必须通过data函数返回一个对象作为组件的状态。