从零开始的vue世界-01

369 阅读5分钟

本章主要介绍vue从零开始到页面呈现的过程

搭建开发环境 rollup

工欲善其事必先利其器,首先我们搭建一个开发环境,这里使用rollup ,与Webpack偏向于应用打包的定位不同,rollup.js更专注于Javascript类库打包

安装:

npm i rollup rollup-plugin-babel @babel/core @babel/preset-env
安装 roolup,安装es6转es5所需的babel, rollup-plugin-babel:表示在rollup中使用babel;@babel/core:会使用bable块;@babel/preset-env:es6与es5等映射关系

package.json

"scripts": {
    "dev": "rollup -cw" // -c指定配置文件;-w监听文件变化
  },

rollup配置文件

// rollup默认可以导出一个对象 作为打包的配置文件
import babel from 'rollup-plugin-babel'
import resolve from '@rollup/plugin-node-resolve'
export default {
    input:'./src/index.js', // 入口
    output:{
        file:'./dist/vue.js', // 出口
        name:'Vue', // global.Vue
        format:'umd', // esm es6模块  commonjs模块  iife自执行函数  umd (commonjs amd)
        sourcemap:true, // 希望可以调试源代码
    },
    plugins:[
        babel({
            exclude: 'node_modules/**' // 排除node_modules所有文件
        }),
        resolve()

    ]
}

.babelrc bable的配置文件

{
    "presets":[
        "@babel/preset-env" // 指定babel
    ]
}

这样就配置好了开发环境, 在src目录下新建index.js

export const a = 1
export default {b: 2}

在dist目录下新建index.html

<body>
    <script src="vue.js"></script>
    <script>
        console.log(Vue)
    </script>
</body>

会发现打印出 { a: 1, default: { b: 2, } };

Vue响应式原理

初始化数据

创建构造函数,使用initMixin来初始化数据,添加_init方法

import { initMixin } from "./init";

// 用Class会将所有的方法都耦合在一起
function Vue(options){ // options就是用户的选项
    this._init(options); // 默认就调用了init
}

initMixin(Vue); // 扩展了_init方法

export default Vue

init.js


export function initMixin(Vue) { // 就是给Vue增加init方法的
    Vue.prototype._init = function (options) { // 用于初始化操作
        // vue  vm.$options 就是获取用户的配置
        // 我们使用的 vue的时候 $nextTick $data $attr.....
        const vm = this;
        vm.$options = options; // 将用户的选项挂载到实例上 后面的initXXX就不需要传option了
    }



然后在index.html中调用

<script>
        const el = new Vue({
            name: '张三',
            age: '18'
        })
        console.log(el)
    </script>

打印出现:

Vue:{$options: {
    age: "18",
    name: "张三"
}}

数据劫持,对象的劫持

在上述数据初始化过程中,来进行数据的劫持 init.js

import { initState } from "./state";

export function initMixin(Vue) { // 就是给Vue增加init方法的
    Vue.prototype._init = function (options) { // 用于初始化操作
        // vue  vm.$options 就是获取用户的配置
        // 我们使用的 vue的时候 $nextTick $data $attr.....
        const vm = this;
        vm.$options = options; // 将用户的选项挂载到实例上

        // 初始化状态
        initState(vm);
    }

}

state.js

import { observe } from "./observe/index";

export function initState(vm) {
    const opts = vm.$options; // 获取所有的选项
    if (opts.data) {
        initData(vm);
    }
}
function initData(vm) {
    let data = vm.$options.data; // data可能是函数和对象
    data = typeof data === 'function' ? data.call(vm) : data; // data是用户返回的对象

    vm._data = data; // 我将返回的对象放到了_data上
    // 对数据进行劫持 vue2 里采用了一个api defineProperty
    observe(data)

    // 将vm._data 用vm来代理就可以了 这样就可以使用vm.age,不需要vm.$options.age
    for (let key in data) {
        proxy(vm, '_data', key);
    }
}
// 将取值进行代理
function proxy(vm, target, key) {
    Object.defineProperty(vm, key, { // vm.name
        get() {
            return vm[target][key]; // vm._data.name
        },
        set(newValue){
            vm[target][key] = newValue
        }
    })
}

observe/index.js

export function observe(data){
    // 对这个对象进行劫持
    if(typeof data !== 'object' || data == null){
        return; // 只对对象进行劫持
    }
    if(data.__ob__ instanceof Observer){ // 说明这个对象被代理过了
        return data.__ob__;
    }
    // 如果一个对象被劫持过了,那就不需要再被劫持了 (要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过)

    return new Observer(data);

}
class Observer{
    constructor(data){
        // Object.defineProperty只能劫持已经存在的属性 (vue里面会为此单独写一些api  $set $delete)
        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false // 将__ob__ 变成不可枚举 (循环的时候无法获取到)这样防止在后面对值的递归死循环
        });
         data.__ob__ = this; // 给数据加了一个标识 如果数据上有__ob__ 则说明这个属性被观测过了
       
    }
    walk(data){ // 循环对象 对属性依次劫持
        // "重新定义"属性   性能差
        Object.keys(data).forEach(key=> defineReactive(data,key,data[key])) // 对所有对象进行属性劫持
    }
}
export function defineReactive(target,key,value){ // 闭包  属性劫持
    observe(value); // 对所有的对象都进行属性劫持
    Object.defineProperty(target,key,{
        get(){ // 取值的时候 会执行get
            console.log('key',key)
            return value
        },
        set(newValue){ // 修改的时候 会执行set
            if(newValue === value) return
            observe(newValue) // 劫持新加的数据
            value = newValue
        }
    })
}


这样就完成了对data的数据劫持,这时打印如下: index.html

<script>
        const el = new Vue({
            data(){
                return {
                    name: '张三',
                    age: 12
                }
            }
        })
        console.log(el)
        console.log(el.name, el.age)
    </script>

image.png

数据劫持,数组的劫持

如果数组里面有非常过的数据,比如一万多数据,会有性能问题,用户一般会通过push,pop等方法来修改数组方法,这样就可以考虑进行对数组的变异方法重写来操作

data._proto_={
    push(){console.log('重写的push')}
}

这样可以重写push但是会将原来的覆盖掉,需要保留数组原有的特性,并且重写原有的部分方法 array.js

// 我们希望重写数组中的部分方法
let oldArrayProto = Array.prototype; // 获取数组的原型
// newArrayProto.__proto__  = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto);

let methods = [ // 找到所有的变异方法
    'push',
    'pop',
    'shift',
    'unshift',
    'reverse',
    'sort',
    'splice'
] // concat slice 都不会改变原数组
methods.forEach(method => {
    // arr.push(1,2,3)
    newArrayProto[method] = function (...args) { // 这里重写了数组的方法
        // push.call(arr)
        // todo...
        const result = oldArrayProto[method].call(this, ...args); // 内部调用原来的方法 , 函数的劫持  切片编程
        // 我们需要对新增的 数据再次进行劫持
        let inserted;
        let ob = this.__ob__;
        switch (method) {
            case 'push':
            case 'unshift': // arr.unshift(1,2,3)
                inserted = args;
                break;
            case 'splice':  // arr.splice(0,1,{a:1},{a:1})
                inserted = args.slice(2);
            default:
                break;
        }
        // console.log(inserted); // 新增的内容
        if(inserted) {
            // 对新增的内容再次进行观测  
            ob.observeArray(inserted);
        }
        return result
    }
})

新的observe.js

import { newArrayProto } from "./array";

class Observer{
    constructor(data){
        // Object.defineProperty只能劫持已经存在的属性 (vue里面会为此单独写一些api  $set $delete)
        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false // 将__ob__ 变成不可枚举 (循环的时候无法获取到)
        });
        // data.__ob__ = this; // 给数据加了一个标识 如果数据上有__ob__ 则说明这个属性被观测过了
        if(Array.isArray(data)){
            // 这里我们可以重写数组中的方法 7个变异方法 是可以修改数组本身的
            data.__proto__ =  newArrayProto  // 需要保留数组原有的特性,并且可以重写部分方法
            this.observeArray(data); // 如果数组中放的是对象 可以监控到对象的变化
        }else{
            this.walk(data);
        }
    }
    walk(data){ // 循环对象 对属性依次劫持
        // "重新定义"属性   性能差
        Object.keys(data).forEach(key=> defineReactive(data,key,data[key]))
    }
    observeArray(data){ // 观测数组
        data.forEach(item=> observe(item))
    }
}
export function defineReactive(target,key,value){ // 闭包  属性劫持
    observe(value); // 对所有的对象都进行属性劫持
    Object.defineProperty(target,key,{
        get(){ // 取值的时候 会执行get
            console.log('key',key)
            return value
        },
        set(newValue){ // 修改的时候 会执行set
            if(newValue === value) return
            observe(newValue)
            value = newValue
        }
    })
}
export function observe(data){
    // 对这个对象进行劫持
    if(typeof data !== 'object' || data == null){
        return; // 只对对象进行劫持
    }
    if(data.__ob__ instanceof Observer){ // 说明这个对象被代理过了
        return data.__ob__;
    }
    // 如果一个对象被劫持过了,那就不需要再被劫持了 (要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过)

    return new Observer(data);

}

这样就实现的数组的劫持,至此开发环境的构建和Vue响应式原理已经实现,下次将会实现模板编译与页面渲染实现。