手写实现vue2学习笔记(一)对象和数组的响应式实现

目录

1. rollup打包工具环境的搭建

1.1install rollup

1.2配置rollup

2. 初始化数据

3. 对象的响应式原理

 4. 对数组进行劫持

4.1 将数组里的每一个对象都添加上响应式

4.2重写7个变异方法



1. rollup打包工具环境的搭建

为什么需要打包?

1. 我们在学习vue2源码的工程中,为了有条理会将js代码写道许多的文件当中,那么在html上引入就需要多个script标签,使用打包过后将许多零碎的文件打包成一个整体,页面只需请求一次,js文件中使用模块化互相引用(export、import ),这样能在一定程度上提供页面渲染效率。

2. 打包同时也将高级语法如es6,箭头函数,let关键字等,转换为es5的语法更好的兼容低级的浏览器。

1.1install rollup

在webstrom软件上,创建一个新项目后,点Terminal

1. npm init 

生成package.json文件,并在文件中更改配置如下图,-c指定配置文件,w表示监控文件更新。

2. npm install rollup rollup-plugin-babel @babel/core @babel/preset-env --save-dev

 npm install rollup :安装rollup,

rollup-plugin-babel:要在rollup里面使用babel

@babel/core:babel的核心模块

@babel/preset-env:高版本语法转成低版本语法

--save-dev:表示开发时依赖

1.2配置rollup

1.在项目目录下创建src目录,然后创建入口文件index.js

2. 在项目目录下创建rollup的配置文件

 3. 在rollup.config.js中写如下配置,然后npm run dev即可有dest文件夹生成

// 导出打包的配置文件
import babel from "rollup-plugin-babel"
export default {
    input: "./src/index.js",//入口
    output: {
        file: "./dist/vue.js",//出口
        name: "Vue", // 在全局上增加一个Vue属性
        format: "umd", // 兼容(common.js amd)
        sourcemap: true // 表示可以调试源代码
    },
    // 表示在打包时依赖的插件
    plugins: [
        babel({
            "presets": [
                "@babel/preset-env"
            ],
            exclude: "node_modules/**"
        })
    ]
}

2. 初始化数据

这个部分讲述的是,对于new Vue之后发生的一些事情,首先我们应该收集依赖

const vue = new Vue({
        data() {
            return {
                name: 'aaa',
                age: 18
            }
        }
    })

我们首先对应就需要一个叫做Vue的构造函数,采用了es5的一种写法function Vue(){} 然后通过Vue.prototype.xxx = function(){} 的方式去增加它的方法。而不是通过es6的class Vue的这种方式去创建一个类

主要的原因就是因为,class的方式需要将方法写在class内,太过于耦合,且不利于分不同js文件去处理

在index.js内的代码

import {initMixin} from "./init";

function Vue(options) {
    this._init(options)
}

initMixin(Vue)

export default Vue

构造函数Vue要求传入一个options的参数,也就是配置对象,然后通过initMixin函数去给他的原型上面加一个_init的初始化方法。由于在另一个文件内找不到这个Vue所以需要将Vue传到initMixin里面

在init.js内的代码

import {initState} from "./state";

export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this
        vm.$options = options
        initState(vm)
    }
}

存储this,然后将options挂载到实例上面,使原型上的方法都可以看到这个options,减少参数传递

在state.js里的代码

export function initState(vm) {
    if(vm.$options.data) {
        initDate(vm)
    }
}
function initDate(vm) {
    let data = vm.$options.data
    data = typeof data === 'function' ? data.call(vm) : data
    console.log(data);
}

初始化状态,查看options里面是否有data,有就初始化data,由于在vue2中,根实例可以是对象也可以是函数,所以对这个data进行一下判断。

3. 对象的响应式原理

经过上面的操作,现在已经拿到了data,那么我们需要对data进行一下观测,也就是对于data进行一个属性劫持

那么我们会使用Object.defineProperty方法,对属性进行重写,为什么要这样做呢?

好处就是Object.defineProperty提供了两个方法,get,set能够在属性值发生变化或者查看属性值时监控到

class Observe {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        // 重写属性
        Object.keys(data).forEach(key => defineActive(data,key,data[key]))
    }
}

export function defineActive(target,key,value) {
    // 如果属性是一个对象,那么就再次调用观测方法
    observe(value)
    Object.defineProperty(target,key,{
        get() {
            return value
        },
        set(newValue) {
            if(newValue === value) return
            value = newValue
        }
    })
}

export function observe(data) {
    // 如果说是对象类型就对他进行观察
    if(typeof data != 'object' || data == null) return

    return new Observe(data);
}

state.js 的代码 

function proxy(vm,target,key) {
    Object.defineProperty(vm,key,{
        get() {
            return vm[target][key]
        },
        set(v) {
         // 如果新值是对象,就代理
            observe(v)
            vm[target][key] = v
        }
    })
}

function initDate(vm) {
    let data = vm.$options.data
    data = typeof data === 'function' ? data.call(vm) : data

    // 将用户数据挂载到实例上
    vm._data = data
    // 观测方法进行数据劫持
    observe(data)
    // 将_data上的属性代理到vm上
    for (const dataKey in data) {
        proxy(vm,'_data',dataKey)
    }
}

 4. 对数组进行劫持

根据上面写的代码,data里的属性是对象时,会导致将数组里的所有元素都添加上,get和set方法,如下图所示

 这样的话看起来好像不错,但是会导致过多的资源浪费。因为用户在操作数组时基本使用的是数组的方法,如push shift等,一般不会使用索引的形式去操作数据,那么我们就需要对数组做另外的处理。

4.1 将数组里的每一个对象都添加上响应式

代码实现

4.2重写7个变异方法

 保留数组的原有特性并重写数组的部分方法,也就是要调用原有的方法,然后参杂着我们自己所需的代码

在observe目录下新建array.js文件


let oldArrayProto = new Array()
// 保留数组原有特性
export let newArrayProto = Object.create(oldArrayProto)

let methods = [
    'push',
    'shift',
    'unshift',
    'pop',
    'reverse',
    'sort',
    'splice'
]

methods.forEach(method =>{
    // 重写部分方法
    newArrayProto[method] = function (...args) {
        let result = oldArrayProto[method].call(this,...args)
        // 需要对新增的属性再次劫持
        let inserted;
        
        let ob = this.__ob__;
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args;
                break
            case 'splice':
                inserted = args.slice(2)
                break
            default:
                break
        }
        // 有添加的内容,就对其增加
        if(inserted) {
            // 添加响应式
            ob.observeArray(inserted);
        }
        return result
    }
})

observe目录下index.js文件新内容

import {newArrayProto} from "./array.js";

class Observe {
    constructor(data) {
        // 添加一个ob属性给数组新添加的元素加响应式用,要调observeArray
        Object.defineProperty(data,'__ob__',{
            value: this,
            enumerable:false
        })
        if(Array.isArray(data)) {
            // 改变数组原型,增加变异方法
            data.__proto__ = newArrayProto;
            this.observeArray(data)
        }else {
            this.walk(data)
        }
    }
    walk(data) {
        // 重写属性
        Object.keys(data).forEach(key => defineActive(data,key,data[key]))
    }
    observeArray(data) {
        data.forEach(item => {
            observe(item)
        })
    }
}

export function defineActive(target,key,value) {
    // 如果属性是一个对象,那么就再次调用观测方法
    observe(value)
    Object.defineProperty(target,key,{
        get() {
            return value
        },
        set(newValue) {
            if(newValue === value) return
            value = newValue
        }
    })
}

export function observe(data) {
    // 如果说是对象类型就对他进行观察
    if(typeof data != 'object' || data == null) return
    // 因为加了一条新的标识,可以对其进行验证是否观测过
    if(data.__ob__ instanceof Observe) return data.__ob__
    return new Observe(data);
}

这样数组的变异方法就重写完毕,可以对其进行测试,接下来会在下一篇博客,写解析模板参数,生成ast语法树,虚拟DOM转真实DOM,敬请期待........

  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值