vue源码分析【1】-new Vue之前

2023-01-01 08:55:25

当前篇:vue源码分析【1】-new 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>
    </div>

    <script>
        debugger;
        var app = new Vue({
            el: '#app',
            beforeCreate() { },
            created() { },
            beforeMount() { },
            mounted: () => { },
            beforeUpdate() { },
            updated() { },
            beforeDestroy() { },
            destroyed() { },
            data: function () {
                return {
                    money: 100,
                    num: 12,
                    arryList: [{name:'子树'}]
                }
            },
            computed: {
                total() {
                    return this.money * this.num;
                }
            },
            methods: {
                getMoreMoney() {
                    this.money = this.money * 2
                    this.arryList.unshift({name: '大树'})
                }
            }
        })
    </script>
</body>
</html>

1. 前言

此篇文章旨在说明new Vue之前做了哪些操作,给Vue构造函数挂载了哪些方法和属性,由于没有进入new Vue,所以不会执行挂载函数,而仅仅是挂载操作。另外,在new Vue之前的代码,意味着此时都不会拿到html中new Vue的入参el,beforeCreate等等。

因此,此文章最大的目的是作为后续文章的参考,注意,由于挂载函数不执行,所以我们在讲解挂载函数时会省略一些代码,详细代码将在后续文章中逐步讲解。

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

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

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


2. 整体流程

// 定义 Vue.prototype._init 方法
initMixin(Vue)
/**
 * 定义:
 *   Vue.prototype.$data
 *   Vue.prototype.$props
 *   Vue.prototype.$set
 *   Vue.prototype.$delete
 *   Vue.prototype.$watch
 */
stateMixin(Vue)
/**
 * 定义 事件相关的 方法:
 *   Vue.prototype.$on
 *   Vue.prototype.$once
 *   Vue.prototype.$off
 *   Vue.prototype.$emit
 */
eventsMixin(Vue)
/**
 * 定义:
 *   Vue.prototype._update
 *   Vue.prototype.$forceUpdate
 *   Vue.prototype.$destroy
 */
lifecycleMixin(Vue)
/**
 * 执行 installRenderHelpers,在 Vue.prototype 对象上安装运行时便利程序
 * 
 * 定义:
 *   Vue.prototype.$nextTick
 *   Vue.prototype._render
 */
renderMixin(Vue)

3. initMixin

作用:

初始化vue,挂载_init方法。

在Vue原型上挂载一个_init方法,这一步只是挂载方法,并没有执行

源码:

//初始化vue
initMixin(Vue);    
var uid$3 = 0;

function initMixin(Vue) {
        Vue.prototype._init = function (options) { //初始化函数
        ...
}
console.log(Vue.prototype)

执行完initMixin,我们看下此时Vue的原型,ok,符合预期,就只挂载了一个_init方法:

{
    _init: ƒ (options),
    constructor: ƒ Vue(options), // 此项是默认的
    __proto__: Object // 此是默认的
}

4. stateMixin

4-1. 基本信息

作用:

数据绑定,挂载 d a t a , data, dataprops, s e t , set, setdelete,$watch方法

流程:

  • 分别定义dataDef对象和propsDef对象,给它们各自挂载一个get方法,分别返回this._data和 this._props
  • 再给dataDef和propsDef分别挂载一个set方法,当改变数据时发出警告。
    • dataDef的set规定:不能直接替换实例根$data
    • propsDef的se规定:不能更改props的值
  • 通过Object.defineProperty给Vue原型挂载对应data和props的get和set方法。
  • 为确保更新数据或者删除数据更更新视图,挂载 s e t 和 set和 setdelete
  • 挂载$watch

源码:

stateMixin(Vue);
function stateMixin(Vue) {
        debugger
        //直接声明的定义对象生成的流在某种程度上有问题
        //在使用Object.defineProperty时,我们必须循序渐进地进行构建
        //  定义一个data对象
        var dataDef = {};
        //重新定义get 和set方法
        dataDef.get = function () {
            return this._data //获取data中的数据
        };
        //  定义一个props对象
        var propsDef = {};
        propsDef.get = function () {
            return this._props// 获取props 数据
        };
        {
            dataDef.set = function (newData) {
                // 修改数据时,避免直接替换实例根$data。
                /**
                 * 以下直接给根$data赋值会警告:
                 *  const list={
                        name:"子树"
                    }
                    this.$data = list;

                    正确的写法:
                    const list={
                        name:"子树"
                    }
                    Object.assign(this.$data,list); 
                 * 
                 */
                warn(
                    'Avoid replacing instance root $data. ' +
                    'Use nested data properties instead.',
                    this
                );
            };
            propsDef.set = function () {
                /**
                 * props 是只读数据,不能更改。
                 * 这里也属于Vue单向数据流的范畴,如父组件能通过props改变子组件,
                 * 但子组件改变传入的props就会警告,只允许单向流动
                 */
                warn("$props is readonly.", this);
            };
        }

        // 数据响应式的关键方法
        // 【说明1】
        Object.defineProperty(Vue.prototype, '$data', dataDef);
        Object.defineProperty(Vue.prototype, '$props', propsDef);

        /** 【说明2】
         * 确保设置值能更新视图
         * 通过 Vue.set 或者 this.$set 方法给 target 的指定 key 设置值 val
         * 如果 target 是对象,并且 key 原本不存在,则为新 key 设置响应式,然后执行依赖通知
        */
        Vue.prototype.$set = set;
        /**
         * 确保删除能更新视图
         * 通过 Vue.delete 或者 vm.$delete 删除 target 对象的指定 key
         * 数组通过 splice 方法实现,对象则通过 delete 运算符删除指定 key,并执行依赖通知
        */
        Vue.prototype.$delete = del;

        /**
         *  挂载$watcher,返回 unwatch函数:
            创建 watcher 实例,如果设置了 immediate,则立即执行一次 cb
         *  返回 unwatch
         *  unwatch 函数: 用于取消 watch 监听
         */
        Vue.prototype.$watch = function (
            expOrFn, //用户手动监听
            cb, // 监听 变化之后 回调函数
            options //参数
        ) {
            var vm = this;
            if (isPlainObject(cb)) { //判断是否是对象 如果是对象则递归 深层 监听 直到它不是一个对象的时候才会跳出递归
                //    转义handler 并且为数据 创建 Watcher 观察者
                return createWatcher(
                    vm,
                    expOrFn,
                    cb,
                    options
                )
            }
            options = options || {};
            options.user = true; //用户手动监听, 就是在 options 自定义的 watch

            //实例化Watcher 观察者
            var watcher = new Watcher(
                vm, //vm  vode
                expOrFn,  //函数 手动
                cb, //回调函数
                options  //参数
            );
            if (options.immediate) {
                //回调触发函数
                cb.call(vm, watcher.value);
            }
            return function unwatchFn() { //卸载观察者,解除监听
                //从所有依赖项的订阅方列表中删除self。
                watcher.teardown();
            }
        };
}

4-2 说明

4-2-1 说明1:Object.defineProperty()

Object.defineProperty(Vue.prototype, ‘$data’, dataDef)的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性。通过get进行依赖收集,而每个set方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

Object.defineProperty(obj, prop, desc)

  • obj 需要定义属性的当前对象
  • prop 当前需要定义的属性名
  • desc 属性描述符
    一般通过为对象的属性赋值的情况下,对象的属性可以修改也可以删除,但是通过Object.defineProperty()定义属性,通过描述符的设置可以进行更精准的控制对象属性

同样,执行完这一行代码,我们再次打印Vue.prototype,如下:

{
    _init: ƒ (options),
    constructor: ƒ Vue(options),
    __proto__: Object,
    $data: undefined, // 本次加上的
    get $data: ƒ (), // 本次加上的
    set $data: ƒ (newData) // 本次加上的
}

4-2-2 说明2:Vue.prototype.$set = set

这个set方法如下:

function set(target, key, val) {...}

它做了如下动作:

  • 通过 Vue.set 或者 this.$set 方法给 target 的指定 key 设置值 val
  • 如果 target 是对象,并且 key 原本不存在,则为新 key 设置响应式,然后执行依赖通知

vue开发时经常会遇到当vue实例已经创建好了,有时候需要再次给数据赋值时,并不能在视图中改变。

比如:

<template>
  <div>
    测试
    {{ list }}
    {{ obj }}
    <Button @click="change">改变数组</Button>
    <Button @click="addValue">改变对象</Button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: [1, 2, 3],
      obj: { name: "子树" },
    };
  },
  methods: {
    change() {
      this.list[1] = 666; // 通过下标直接改变数组
      console.log(this.list);
    },
    addValue() {
      this.obj.value = true; // 给对象添加一个属性
      console.log(this.obj);
    },
  },
};
</script>

显示如图:

image.png

我们会发现,控制台打印的数据已经更新了,但是,视图并没有同步更新。那么,我们怎么让视图同步更新呢?

通过Vue.set(target, key, val)来改变:

  • target: 要改变的对象或者数组
  • key: 数组的下标或者对象的key
  • val: 赋值的新的值
  methods: {
    change() {
    //   this.list[1] = 666;
      Vue.set(this.list, 1, 666)
      console.log(this.list);
    },
    addValue() {
    //   this.obj.value = true;
      Vue.set(this.obj, 'value', true)
      console.log(this.obj);
    },
  },

此时,我们再回到页面,可以看到控制台和页面都同步更新了


4-3. 结果

stateMixin(Vue)执行完后,我们再打印Vue.prototype结果如下:

{
    $delete: ƒ del(target, key), //本次加上的
    $set: ƒ (target, key, val),  //本次加上的
    $watch: ƒ ( expOrFn), //本次加上的
    $data: undefined,  //本次加上的
    $props: undefined, //本次加上的
    get $data: ƒ (),  //本次加上的
    set $data: ƒ (newData),  //本次加上的
    get $props: ƒ (),  //本次加上的
    set $props: ƒ (),  //本次加上的
    _init: ƒ (options),
    constructor: ƒ Vue(options),
    __proto__: Object,
}

5. eventsMixin

5-1. 基本信息

作用:

初始化事件绑定方法

流程:

  • 依次挂载$on,$once,$off,$emit事件相关的属性。

源码:

// 以下各个挂载事件将在各个小节讲解
function eventsMixin(Vue) {
        var hookRE = /^hook:/;  //开头是hook: 的字符串
        
        /**
         * 监听Vue自定义事件,把所有事件拆分存放到_events 数组中,返回Vm
         */
         Vue.prototype.$on = function (){...}
         
         /**
         * 监听Vue自定义事件,返回Vm。
         * once比较特殊,只触发一次,然后就会移除监听器
         */
         Vue.prototype.$once = function (event, fn) {...}
         
         /**
         * vue把事件添加到一个数组队列里面,通过删除该数组事件队列,而达到解绑事件
         */
          Vue.prototype.$off = function (event, fn) {...}
          
         /**
         *  触发事件
         */
         Vue.prototype.$emit = function (event) {...}
}

5-2. $on

作用:

监听Vue自定义事件,把所有事件拆分存放到_events 数组中,返回Vm

流程:

  • 如果传入的事件名event为数组,则遍历,递归调用$on
  • 否则,如果传入的事件名event为单个事件名,把所有事件拆分存放到_events 数组中,比如传入的eventclickfnaddNumvm._events['click']原本是[],则结果如下:vm._events = { click: [addNum] },然后返回vm
    • 如果事件名是以 hook: 开头的(如hook:mount=‘getNum’),则标记为vue系统内置钩子函数(比如vue 生命周期函数等)

源码:

Vue.prototype.$on = function (
            event, // 单个的事件名称或者有多个事件名组成的数组
            fn // 回调
            ) {
            var this$1 = this;
            var vm = this;
            // 【逻辑 1】 event为多个事件组成的数组
            if (Array.isArray(event)) {
                for (var i = 0, l = event.length; i < l; i++) {
                    // event为事件数组,则遍历,递归调用$on
                    this$1.$on(event[i], fn);
                }
            }
            // 【逻辑 2】 event为单个事件名
            else {
                /**
                 * 把所有事件拆分存放到_events 数组中
                 * vm._events本身是个对象,
                 * 比如传入的event为click,fn为addNum,vm._events['click']原本是[],则结果如下:
                 * vm._events = { click: [addNum] }
                 */
                (vm._events[event] || (vm._events[event] = [])).push(fn);
                /**
                 * 如果事件名是以 hook: 开头的(如hook:mount='getNum'),
                 * 标记为vue系统内置钩子函数, 比如vue 生命周期函数等
                 */
                if (hookRE.test(event)) {
                    vm._hasHookEvent = true;
                }
            }
            return vm
};

5-3. $once

作用:

监听Vue自定义事件,先把传入的事件在vm上解绑,然后执行传入的事件。

虽然最终也触发$on,但once比较特殊,只触发一次,然后就会移除监听器

流程:

  • 创建一个封装事件on,用于解绑和执行事件
  • 将传入的事件挂载到封装事件on上,然后调用vm.$on(event, on);
  • 接着就是$on的逻辑了,也就是说,$once最终还是会走到$on

源码:

        Vue.prototype.$once = function (event, fn) {
            var vm = this;
            function on() {
                // 解绑事件
                vm.$off(event, on);
                // 执行回调事件
                fn.apply(vm, arguments);
            }
            // 将传入的回调函数fn挂载到我们定义on函数的fn上。
            on.fn = fn;
            // 再把on添加到vm上
            vm.$on(event, on);
            return vm
        };


5-4. $off

作用:

vue把事件添加到一个数组队列里面,通过删除该数组事件队列,而达到解绑事件。

返回删除了_events属性中相应事件的vm。

流程:

  • 如果没有参数的情况下,移除所有监听器
  • 否则,如果传入的event是数组事件 则循环回调递归Vue.$off
  • 否则,vm._events中,不存在传入的事件名
  • 否则,如果回调函数不存在则清空 _events对应的事件名中所有事件
  • 否则,在事件cbs(vm._events中传入的event属性)数组中移除我们指定的回调函数

源码:

        Vue.prototype.$off = function (event, fn) {
            var this$1 = this;
            var vm = this;
            // 【逻辑 1】 如果没有参数的情况下,移除所有监听器
            if (!arguments.length) {
                // 赋值一个没有原型的空对象
                vm._events = Object.create(null);
                return vm
            }
            // 【逻辑 2】  如果传入的event是数组事件 则循环回调递归Vue.$off
            if (Array.isArray(event)) {
                for (var i = 0, l = event.length; i < l; i++) {
                    this$1.$off(event[i], fn);
                }
                return vm
            }
            //  【逻辑 3】,vm._events中,不存在传入的事件名
            var cbs = vm._events[event];
            if (!cbs) {
                return vm
            }
            // 【逻辑 4】,如果回调函数不存在则清空 _events对应的事件名中所有事件
            if (!fn) {
                vm._events[event] = null;
                return vm
            }
            // 【逻辑 5】在事件cbs(vm._events中传入的event属性)数组中移除我们指定的回调函数
            if (fn) {
                var cb;
                var i$1 = cbs.length;
                while (i$1--) {
                    // 拿到vm._events中event事件名下每个事件
                    cb = cbs[i$1];
                    // cb.fn为了兼容$once中on.fn = fn;
                    if (cb === fn 





							
  • 作者:南城夏季
  • 原文链接:https://blog.csdn.net/weixin_39818813/article/details/118494537
    更新时间:2023-01-01 08:55:25