一、rollup的环境搭建
是一个js模块打包工具,一般项目的类库打包全部采用rollup,因为打包体积会比webpack小
- 首先一个空文件夹,初始化生成
packagejson文件
npm init -y
- 安装
roullup,用来打包js库
npm i roullup
- 需要使用高级语法还要安装 babel,
@babel/core是babel默认插件,@babel/preset-env高版本语法转低版本
npm i rollup-plugin-babel @babel/core @babel/preset-env
- 创建文件
rollup.config.js,执行打包命令时会找这个文件,默认找这个文件
- 在
package.json->scripts下写脚本
如:"dev":"rollup -cw" -c为使用第四步的配置文件 w为监控文件变化
- 创建入口文件
src->index.js
- 指定打包入口为步骤6的路径
rollup.config.js中
//babel插件
import babel from 'rollup-plugin-babel'
// rollup默认可以导出一个对象,作为打包的配置文件
export default {
//入口
input:'./src/index.js',
//出口
output:{
file:'./dist/vue.js',
//在全局上增加Vue,在global上添加属性叫Vue,在页面上能new Vue
name:'Vue',
//打包格式,常见的有esm es6模块 commonjs模块 iife 自执行函数 umd(统一模块规范,兼容amd,commonjs等)
format:'umd',
//可以调试源代码
sourcemap:true
},
//插件
plugins:[
//babel插件
//一般用babel都会创建一个配置文件
babel({
//排除不需要打包的
exclude:'node_modules/**'
})
]
}
- babel配置文件位于根目录下,可以是js的或者rc的 如rc:
.babelrc
{
"presets":[
"@babel/preset-env"
]
}
打包测试
npm run dev步骤6 配置的
二、初始化数据
完成上面的步骤后就希望把data做一个代理,因此需要一个Vue的类因为new了它
在vue的源码里并没有用class Vue{}的方式创建类,因为这样写的话所有的方法会耦合在一起
因此使用的是构造函数 function Vue(){}
// src/index.js中
function Vue(options){ // options就是用户的选项
}
export default Vue
那么用原型方式去写代码的话,会将代码封装在原型上Vue.prototype.***
function Vue(options){
this._init(options)
}
Vue.prototype._init = function(options){
//用于数据初始化
}
export default Vue
但是功能一多,还是会不停的加,那就耦合了,我们肯定希望每个功能是独立的,独立成一个文件
1、初始化文件
新建一个文件 如:init.js(做初始化的)
那么上面的这句就不能复制过来,因为文件不同了就没有Vue了
Vue.prototype._init = function(options){ //用于数据初始化 }
所以,可以用函数将Vue传过来
export function initMixin(Vue){ // 给Vue增加init方法
Vue.prototype._init = function(options){
//用于数据初始化
}
}
然后在入口文件index.js中直接导入调用、传入Vue即可initMixin(Vue)
import { initMixin } from "./init"
function Vue(options){
this._init(options)
}
initMixin(Vue)// 扩展了init方法
export default Vue
2、扩展其他方法
- 那么在初始化文件中,想扩展其他方法
export function initMixin(Vue){
Vue.prototype._init = function(options){
// 2.那么就要考虑将options放在实例上
// this.$options = options 为什么要加$,表示是Vue里面的变量
const vm = this
vm.$options = options // 3.将用户的选项挂载到实例上
}
Vue.prototype.XXX = function(){
// 1.是拿不到_init的options
}
}
- 然后我们主要做的就是要对数据进行处理
export function initMixin(Vue){
Vue.prototype._init = function(options){
const vm = this
vm.$options = options
// 初始化状态
initState(vm)
}
}
function initState(vm){
const opts = vm.$options // 1.获取所有选项
// 1.1 先做Data,如果有Data属性就做初始化,继续传vm
if(opts.data){
initData(vm)
}
}
function initData(vm){
// 2. 数据代理
let data = vm.$options.data // 在vue2中data可能是函数或者对象 ,vue3中data就是函数
// 2.1 判断data的类型
data = typeof data === 'function' ? data.call(vm) : data
}
当然initState()、initData()也可以提取出来
三、实现对象的响应式原理、对象属性劫持
完成上面的代码后怎么去做数据劫持呢?在initData()中去实现
function initData(vm){
let data = vm.$options.data
data = typeof data === 'function' ? data.call(vm) : data
// 对数据进行劫持vue2 里采用了一个API defineProperty
observe(data)
}
这个函数当然也能放在其他文件,做好导入导出即可
class Observer{
constructor(data){
// 2.1 Object.defineProperty只能劫持已经存在的属性,后增的或删除的是不知道的(vue2里面会为此单独写一些api)
this.walk(data)
}
walk(data){
// 循环对象,对属性依次劫持
// 重新定义属性,定义为响应式对象,因此性能差
Object.keys(data).forEach(key=>defineReactive(data,key,data[key]))
}
}
// 属性劫持
export function defineReactive(target,key,value){ // 闭包
Object.defineProperty(target,key,{
get(){ // 取值时,会执行get
return value
},
set(newValue){ // 修改时,会执行set
if(newValue === value) return
value = newValue
}
})
}
export function observe(data){
// 对这个对象进行劫持
// 1.判断数据的类型
if(typeof data!== 'object' || null){
return // 只对 对象进行劫持
}
// 2.如果一个对象被劫持过,那就不需要再被劫持了(要判断一个对象是否劫持过,可以增添一个实例,用实例判断是否被劫持过)
return new Observer(data)
}
此时在页面中打印vm的时候,并没有刚才劫持过的属性
问题在于:
function initData(vm){
let data = vm.$options.data
data = typeof data === 'function' ? data.call(vm) : data
vm._data = data//2.我们就考虑在vm上增加一个_data,相当于将对象放在了实例上,并且把对象用下面的函数进行了观测
observe(data)// 1.在一步的时候,只是将data传进去了
}
当在取值的时候要:vm._data.name,那怎么使用 vm.name来取值呢?
function initData(vm){
let data = vm.$options.data
data = typeof data === 'function' ? data.call(vm) : data
vm._data = data
observe(data)
// 将 vm._data 用vm来代理就可以了
for(let key in data){
proxy(vm,'_data',key)//自己写的proxy函数
}
}
function proxy(vm,target,key){
Object.defineProperty(vm,key,{
get(){
return vm[target][key]
},
set(newValue){
vm[target][key] = newValue
}
})
}
但此时还不能监控复杂类型,如何实现呢?在属性劫持这一步递归调用observe()
// 属性劫持
export function defineReactive(target,key,value){
observe(value)// 对所有对象进行属性劫持
Object.defineProperty(target,key,{
get(){
return value
},
set(newValue){
if(newValue === value) return
observe(value)//防止修改复杂类型时,传入对象 如:vm.address={num:20}
value = newValue
}
})
}
四、实现数组的函数劫持
数组的话,运用上面的数据劫持,会将数组里面的每个值都加上get()、set()
虽然去改数组的值也能触发值的更新,但一旦数组有成千上万个值,那这样此代码在内部循环时就会走多次
而且用户也很少使用 比如:vm.hobby[888] = 100 去更改,很少用索引去操作数组,但内部做劫持会浪费属性,浪费性能
用户一般修改数组都是通过方法修改,如: push shift.....
所以就考虑如果是数组就不要做循环了,性能太差
将前文代码进行如下更改
import { newArrayProto } from './array'
class Observer{
constructor(data)
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false // 将__ob__变为不可枚举(循环的时候无法获取)
})
// data.__ob__ = this // 好处:1.为下面新文件内容做铺垫,2.给数据加了一个标识,如果数据上有__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 observe(data){
if(typeof data!== 'object' || null){
return
}
if(data.__ob__ instanceof Observer){
// 说明这个对象被代理过了
return data.__ob__
}
return new Observer(data)
}
新文件内容
let oldArrayProto = Array.prototype // 获取数组的原型
// newArrayProto可以通过链拿到oldArrayProto(newArrayProto.__proto__ = oldArrayProto),不用担心会被覆盖掉
export let newArrayProto = Object.create(oldArrayProto)
let methods = [
// 找到所有的变异方法
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
]
methods.forEath(method=>{
newArrayProto[method] = function(...args){ // 这里重写了数组的方法
const 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)
default:
break
}
if(inserted){
// 对新增的内容再次进行观测
ob.observeArray(inserted)
}
return result
}
})
五、解析模板参数
如果想去在页面上取值就可以用插值表达式 如:{{ name }}
所以需要对模板进行编译
那么首先想到的就是要将 模板里面的数据进行替换
方式:
- 模板引擎(性能差,需要正则比配替换,vue 1.0 的时候没有引入虚拟DOM的概念)
- 采用虚拟DOM(数据变化后比较虚拟DOM的差异,最后更新需要更新的地方)
核心就是我们需要将模板变成我们的JS语法,通过JS语法生成虚拟DOM
所以从一个东西变成另一个东西,语法之间的转换,涉及到我们需要先变成语法树,再重新组装代码,成为新的语法 将template语法转成render函数
1
装态初始化完了,要看用户传的值中有没有el属性
// init.js
import { initState } from "./state"
export function initMixin(Vue){
Vue.prototype.__init = function (options){
const vm = this
vm.$options = options
initState(vm)
if(options.el){
vm.$mount(options.el)// 实现数据的挂载
}
}
Vue.prototype.$mount = function(el){
}
}
当然也可以不用el,走的是一样的逻辑
然后会去判断当前用户有没有写template或者render,没有写的话就用#app
Vue.prototype.$mount = function (el) {
const vm = this
el = document.querySelector(el)
let ops = vm.$options
if (!ops.render) {// 先找有没有render函数
let template // 没有render看一下是否写了template,没有就采用外部的#app
if (!ops.template && el) {
// 没有模板但是有el,就用#app
template = el.outerHTML
} else {
if (el) {
template = ops.template // 如果有el,则采用模板的内容
}
}
// 写了就用写了的template
if(template){
// 这里需要对模板进行编译
const render = compileToFunction(template)
ops.render = render
}
}
// 最终就可以获取render方法
ops.render
}
2、
创建src->compiler->index.js
内容为:
// 对模板进行编译处理
export function compileToFunction(template){
// 1.就是将template 转成ast语法树
// 2.生成render方法(返回的结果就是虚拟DOM)
}