当前位置:   article > 正文

Vue2源码 - 响应式原理实现_vue2响应式源码

vue2响应式源码

跟着本文,一起手写响应式原理。

先初始化一个项目,

npm init -y
  • 1

安装一下webpack以及相关依赖,

npm install -D webpack webpack-cli webpack-dev-server html-webpack-plugin
  • 1

安装过程中,新建一个webpack.config.js文件,

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin")

module.exports = {
    entry: {
        index: path.resolve(__dirname, "./src/index.js")
    },
    output: {
        filename: "[name].js",
        path: path.resolve(__dirname, "./dist")
    },
    devtool: "source-map",
    resolve: {
        modules: [path.resolve(__dirname, ""), path.resolve(__dirname, "./node_modules")],
    },
    mode: "none",
    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, "./src/index.html")
        })
    ]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

最基本的webpack配置

按照定义的入口文件路径,根目录下新建src,src下新建index.html,index.js

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在index.js中,vue项目一般写法这时候是先引入vue,再创建一个vue实例,我们这里进行模拟,有地方就需要小改一下,

import Vue from "./vue";

let vm = new Vue({
    el: "#app",
    data() {
        return {
            title: "Student List",
            classNum: 1,
            total: 2,
            teachers: ["Mrs.Wang", "Mis.Chen"],
            students: [
                { id: 1, name: "jack" },
                { id: 2, name: "tom" },
            ]
        }
    }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这里就代表着Vue是从一个叫vue的文件夹那引入的,如果想要完全和vue项目写法一直,如下面这样:

import Vue from "./vue";
  • 1

这就依赖于webpack中配置:

resolve: {
    modules: [path.resolve(__dirname, ""), path.resolve(__dirname, "./node_modules")],
},
  • 1
  • 2
  • 3

这个配置表示,模块解析的路径先从node_modules找文件,如果找不到再从当前目录下找。

vm是Vue的一个new的实例对象,在ES6之下,说明Vue是个构造函数,函数中传入的参数都是一个一个像配置一样,正如此vue是option api。

现在来创建vue文件夹,里面创建个index.js

function Vue(options) {
    this._init(options);
}
export default Vue;
  • 1
  • 2
  • 3
  • 4

在Vue的原型上挂载一个初始化方法,将options配置项作为参数传入。

Vue.prototype._init = function(options) {
    var vm = this;
    vm.$options = options;
    initState(vm);
}
  • 1
  • 2
  • 3
  • 4
  • 5

将this实例保存到vm上,避免this混乱,然后在实例上挂载一个$options,存放参数配置项,初始化的逻辑操作,单独一个文件init.js中处理,创建一个init.js引入initState方法。

那init.js中首先得有个initState函数,

function initState(vm) {
    var options = vm.$options;
    if (options.data) {
        initData(vm);
    }
}
export {
    initState,
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

取实例对象上挂载的参数配置项,然后分模块处理props,methods,data,computed,watch,本文只写响应式,就只有个data,实际vue中的顺序执行:props > methods > data > computed > watch

那initData方法就是处理data响应式逻辑,这里做了两件事

  1. 数据拦截
  2. 观察者模式,也是响应式的核心
function initData(vm) {
    var data = vm.$options.data;
    vm._data = data = typeof data === "function" ? data.call(vm) : data || {}
    for(var key in data) {
        proxyData(vm, "_data", key);
    }
    observe(vm._data);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

先获取挂载在实例对象上参数配置项里的data数据,然后实例上挂载一个_data存放data中数据作为备份,到那时这里需要判断一下data的类型是不是函数,官方推荐data写成函数有返回值格式,因为这样能保证每一份实例对象都有独立的data,不至于组件复用时候造成数据污染。当然也允许直接写成对象的格式。

这时候data中存的就是data() 中return的值,_data作为备份方便实例直接获取,也就是vue中为啥能够直接this点上data中数据的原因。

循环data进行数据拦截,将数据拦截的处理单独放入一个文件中,新建一个proxy.js,这里的proxy不是ES6的Proxy,实际上是用的Object.defineProperty来实现的。

Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

参数:

obj:要定义属性的对象

prop:要定义或修改的属性的名称或 Symbol

descriptor:要定义或修改的属性描述符

proxyData接收三个参数,vm是当前实例对象,target是备份的data数据,key就是需要访问或修改的属性,那么通过target[key]就能拿到这个属性的值。

proxy.js

function proxyData(vm, target, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[target][key];
        },
        set(newValue) {
            vm[target][key] = newValue;
        },
    })
}

export default proxyData;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

descriptor中有一个get方法和一个set方法,但不止这两个。

每次访问vm的属性key,都会触发get方法,我们让它返回这个属性的值,这就相当于拦截了vm.key,得到的值是vm[’_data’].key

每次修改vm中属性key,都会触发set方法,set能拿到最新修改的值,将属性值设为这个最新值。

下面来实现响应式的核心observe,单独一个observe.js文件来处理,

function observe(data) {
    if (typeof data !== "object" || data === null) return
    return new Observer(data);
}

export default observe;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

观察的一定是对象,注意typeof null也是object,这么说来其实也就是观察的object对象和array数组,这里返回了Observer构造函数,在其中进行了对象和数组的判断再处理,然后就是递归,一层一层的再回来判断是不是对象类型,一直到不是为止。

在observer.js中来写这个Observer构造函数,

function Observer(data) {
    if (Array.isArray(data)) {
        // 数组操作
    } else {
        // 对象操作
    }
}
export default Observer;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Object.defineProperty只作用对象,如果对数组也使用这个方法进行观察,那数组极大时就很耗性能,vue采用的是重写数组的七个方法,所以这里区分了对象和数组的处理。

开始对象的拦截处理。

在Observer的原型上定义个walk的方法,传入实例对象中data的返回值。源码中很多方法都喜欢写成walk,代表有循环的意思。

Observer.prototype.walk = function (data) {
    var keys = Object.keys(data);

    for (var i = 0; i < keys.length; i++) {
        var key = keys[i];
        var value = data[key];

        defineReactiveData(data, key, value);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

先拿到data中所有属性,循环拿到每个key和value,具体的逻辑在defineReactiveData中实现。

创建个reactive.js文件,

function defineReactiveData(data, key, value) {
    Object.defineProperty(data, key, {
        
    })
}
export default defineReactiveData;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

太熟悉了,之前处理proxyData也是一样的套路,data是当前实例对象中data对象(函数返回对象),key是需要修改的属性,value是key的值。

具体的get和set处理如下:

get() {
    console.log(`响应式数据获取:`, value);
    return value;
},
set(newValue) {
    console.log('响应式数据设置:', newValue);
    if (newValue === value) return;
    observe(newValue);
    value = newValue;
},
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

获取data中key时,会触发它的get方法,直接返回这个key的value值,当然在实际源码中这儿要做好复杂的其他处理。

设置key的新值时,触发set方法能拿到最新修改的值,如果相同啥事不做,如果不同就把这个新值赋给key的value,但需要注意的是在赋值之前需要判断,如果设置的新值还是对象时,也需要再去观察,调用observe把当前的新值传进去,就又回到了这儿,

function observe(data) {
    if (typeof data !== "object" || data === null) return
    return new Observer(data);
}
  • 1
  • 2
  • 3
  • 4

这就形成了递归。但在刚进入defineReactiveData这个方法时,就需要先调用observe进行判断,因为当前观察的值也可能是对象时候,需要递归的去观察,这样递归观察对象更完善了。

完整的defineReactiveData方法如下:

function defineReactiveData(data, key, value) {
    observe(value);
    Object.defineProperty(data, key, {
        get() {
            return value;
        },
        set(newValue) {
            if (newValue === value) return;
            observe(newValue);
            value = newValue;
        },
    })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

开始数组的拦截处理。

数组的通过重写方法处理的,在原型链上新增自己写的方法,根据原型链的顺序,优先取这些方法处理数组,如果数组中还有数组也需要递归观察。

创建一个array.js

将数组的所有原型方法存在originArrMethods上,但这个是引用,需要新造一个包含是所有数组原型方法的对象,以方便改写。

var originArrMethods = Array.prototype;
var arrMethods = Object.create(originArrMethods);
  • 1
  • 2

然后就是7个会修改数组自身数据的方法,可以把方法名放在一个文件中,config.js

var ARR_METHODS = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse",
];

export {
    ARR_METHODS,
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

就是如上这些方法操作数组会改变原数组,

  1. push:尾部塞入

    var a = [1,2,3];
    a.push(666); // 4
    console.log(a); // [1,2,3,666]
    
    • 1
    • 2
    • 3
  2. pop:尾部导出

    var a = [1,2,3,666];
    a.pop(); // 666
    console.log(a); // [1,2,3]
    
    • 1
    • 2
    • 3
  3. shift:头部导出

    var a = [1,2,3];
    a.shift(); // 1
    console.log(a); // [2,3]
    
    • 1
    • 2
    • 3
  4. unshift:头部塞入

    var a = [2,3];
    a.unshift(1); // 3
    console.log(a); // [1,2,3]
    
    • 1
    • 2
    • 3
  5. splice

  6. sort:排序

  7. reverse:反转

回到源码,

找到config中定义的方法名进行方法重写,

ARR_METHODS.map(function (m) {
    arrMethods[m] = function () {
        var args = Array.prototype.slice.call(arguments);
        var rt = originArrMethods[m].apply(this, args);

        var newArr;

        // 数组新增项可能是数组或对象
        switch (m) {
            case "push":
            case "unshift":
                newArr = args;      // [{a: 1}] / [[1]]
                break;

            case "splice":      // splice(0, 1, {})
                newArr = args.slice(2);     // [{a: 1}, {b: 2}]
                break;
            default:
                break;
        }
        newArr && observeArr(newArr);
        return rt;
    }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这七个方法,每个参数个数都不是固定的,例如splice就可以有3个,将arguments由类数组转成数组,然后对数组方法的拦截,需要在原型方法执行后获取值后进行其他逻辑的操作。

特殊处理新增的方法,它们是需要传新增值,newArr存在时,排除了splice第三个参数没写情况,对新增加的值进行观察。

observeArr方法是遍历数组中每一项,进行观察。新建一个observeArr.js

function observeArr(arr) {
    for (let i = 0; i < arr.length; i++) {
        observe(arr[i]);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5

这里又调用了observe观察数组每一项,判断每项是不是object类型,再进而判断是对象还是数组,这又形成了递归,这样就能将数组中每项每层都响应式处理。

文中代码有的文件的引用没写,可以访问
https://github.com/Zuowendong/sourceCode-of-VueDemo/tree/master/Data
查看,和vue源码中有些方法名并不一致,可以配合vue源码一起学习。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号