本章主要介绍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>
数据劫持,数组的劫持
如果数组里面有非常过的数据,比如一万多数据,会有性能问题,用户一般会通过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响应式原理已经实现,下次将会实现模板编译与页面渲染实现。