Vue2源码 —— 数据响应式实现
配置项
//package.json
{"name": "vue","version": "1.0.0","main": "index.js","scripts": {"dev": "rollup -cw"},"author": "","license": "ISC","description": "","devDependencies": {"@babel/core": "^7.24.3","@babel/preset-env": "^7.24.3","rollup": "^2.79.1","rollup-plugin-babel": "^4.4.0"},"dependencies": {"@rollup/plugin-node-resolve": "^15.2.3"}
}
// rollup.config.js
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',format: 'umd' , sourcemap:true // 希望可以调试源代码},plugins: [babel({exclude: 'mode_modules/**' //排除node_modules的所有文件}),resolve()]
}
-
在rollup.config.js文件中, format 配置项用于指定输出的模块格式。其可选的类型有以下几种:
- “amd”: Asynchronous Module Definition, 适用于浏览器环境和 RequireJS 等 AMD 模块加载器。
- “cjs”: CommonJS 格式, 适用于 Node.js 环境。
- “es” 或 “esm”: ES module 格式,用于支持 ECMAScript 模块。
- “iife”: Immediately Invoked Function Expression, 适用于浏览器环境。
- “umd”: Universal Module Definition, 可同时用于浏览器和 Node.js 环境。
-
配置文件写完之后,就可以写接下来的方法和属性了
文件
-
文件目录
- dist
- test.html // 测试文件
- node_modules
- src
- index.js
- dist
-
index.js
index.js作为入口文件,作为rollup.config.js
的入口。在这个文件中创建一个Vue的函数,然后将这个函数抛出去,作为构造文件,在这个文件中,会引入外部文件。这些外部文件是对这个Vue的原型进行修改的这是文件的最开始的项目准备
响应式数组实现
在vue2中,新建一个Vue的实例,命名为vm,在控制台中输出
vm.$data === vm._data
输出结果为true。说明在Vue2中,私有方法和公有方法的data实现都是一个对象,只不过是挂载的对象不同 -
下面的代码实现的功能
-
将Vue创建的时候传入的参数,捕获,并且传递到init属性上
-
将实例保存到$options属性上
-
将el挂载到$mount属性上
index.html<html> <head> <meta charset="UTF-8"> <meta name="viewport"content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script src="vue.js"></script> <script> const vm = new Vue({data() {return {name: 'zf',age: 20,address: {num: 30,content: 'xiaokeai'},hobby: ['slepp', {a: 1}]}},// el: "#app" }) </script> </body> </html>
init.js ```javascript export function initMixin(Vue) {Vue.prototype._init = function (options) {const vm = thisvm.$options = optionsif (options.el) {vm.$mount(options.el)}} }
index.js
import { initMixin } from "./init" function Vue (options) { // options用户的选项console.log(options)this._init(options) } initMixin(Vue) export default Vue
- 在官方文档中,如果在vue实例化的时候,没有收到el选项,则会处于未挂载状态,没有关联的dom,可以使用vm.$mount()手动的挂在一个未挂载的实例,这里有el选项,所以,会自动挂载。后续手动挂载,在后面实现
-
-
下面代码实现的功能
-
将第一层数据绑定了get和set方法(文件转换太频繁,最后放完整的zip代码包吧,这里仅仅将一些关键代码罗列)
class Observe {constructor(data) {this.walk(data)}walk(data) {Object.keys(data).forEach(key => {definedReactive(data,key,data[key])})}observeArray(data) {data.forEach(item => observe(item))} } export function definedReactive(target, key, value) {Object.defineProperty(target, key, {get() {return value},set(newValue) {if(value === newValue) returnvalue = newValue}}) } export function observe(data) {return new Observe(data) }
在上面的代码中,仅仅是将第一层数据加上了get和set方法,如果想要给多层加上get和set方法,还需要对代码进行改写
-
下属的代码实现了的功能
-
将_data的get和set方法挂在到Vue实例上。 (之前的时候,将_data方法)
-
对数组进行监听,不使用遍历监听每个数据,对性能有很大的浪费。
-
将observer放到__ob__属性中。实现了响应式
// array.js let oldArrayProto = Array.prototype export let newArrayProto = Object.create(oldArrayProto) let methods = ['push','pop','shift','unshift','reverse','sort','splice' ] console.log(newArrayProto) methods.forEach(method => {newArrayProto[method] = function (...args) {const result = oldArrayProto[method].call(this, ...args)let insertedlet ob = this.__ob__switch (method) {case 'push':case'unshift':inserted = argscase 'splice' :inserted = args.slice(2)default:break}if (inserted) {ob.observeArray(inserted)}return result} })
// index.js import {newArrayProto} from "./array";
class Observer {
constructor(data) {data.__ob__ = thisObject.defineProperty(data, '__ob__', {value:this,enumerable:false})if (Array.isArray(data)) {data.__proto__ = newArrayProto} else{this.walk(data)}
}
walk(data) {Object.keys(data).forEach(key => {definedReactive(data,key,data[key])})
}
observeArray(data) {data.forEach(item => observe(item))
}
}
export function definedReactive(target, key, value) {
observe(value)
Object.defineProperty(target, key, {get() {return value},set(newValue) {if(value === newValue) returnobserve(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)
} -
- 下面代码实现了
-
对data的类型进行判断,只有符合为object类型,才能对object进行挂载,将object中的数据都挂载到_data上
import { observe} from "./observe" export function initState(vm) {const ops = vm.$optionsif(ops.data) {initData(vm)} } function proxy(vm, target, key) {Object.defineProperty(vm, key, {get() {return vm[target][key]},set(value) {vm[target][key] = value}}) } function initData(vm) {let data = vm.$options.datadata = typeof data === 'function' ? data.call(vm):data // 转换为对象类型vm._data = data //挂在到实例上observe(data)for (const dataKey in data) {proxy(vm, '_data', dataKey)} }
-
-