vue 实现个简易版(1)

223 阅读3分钟

从零开始一步一步实现个简易版vue,包含响应式,computed,watch,methods等原理。

MVVM原理

Vue2.0 响应式原理最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的;

关于Vue响应式机制,Object.defineProperty()之前的文章 vue 响应式机制简述 已经介绍过了

而要实现Vue,需要从以下几步开始:

  1. 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
  2. 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
  3. 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

初始代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<div id="app">
		<div id="school" data="123">{{school}}</div>
		<ul>
			<li>{{student.name}}</li>
			<li>{{student.age}}</li>
			<li>{{hello}}</li>
		</ul>
	</div>
</body>
<script>

class Vue{
	constructor(options){
		this.$el = options.el || '#app'
		this.$data = options.data() || {}
		this.$computed = options.computed || {}
		this.$watch = options.watch || {}
		this.$methods = options.methods || {}
		
	}
}

const vm = new Vue({
	el: '#app',
	data() {
		return {
			school: "花园小学",
			address: "四川",
			student: {
				age: 18,
				name: '张三'
			}
		}
	},
	computed: {
		hello() {
			return `你好我是${this.student.name},今年${this.student.age}岁。`;
		}
	},
	watch: {
		school(newVal,oldValue) {
			console.log(newVal,oldValue);
			// console.log(`欢迎来到${this.school}!`)
		}
	},
	methods: {
		addAge() {
			this.student.age++
		},
		decreaseAge(){
			this.student.age--
			this.school += 1
		},
		changeInputValue(e){
			this.student.name = e.target.value 
		}
	}
})


</script>
</html>

1. 实现Observer

原理:遍历data通过Object.defineProperty方法,给data里面每个属性设置gerter和setter。

function isObject(obj) {
	return typeof obj === 'object'
		&& !Array.isArray(obj)
		&& obj !== null
		&& obj !== undefined
}
//1.数据劫持
class Observer{
	constructor(data){
		this.observer(data)
	}
	observer(data){
		//如果data存在且类型是object类型
		if(isObject(data)){
			Object.keys(data).forEach((key)=>{
				this.defindReactive(data,key,data[key])
			})
		}
	}
	defindReactive(obj,key,value){
    	this.observer(value) //如果数据是一个对象,做成响应式
		Object.defineProperty(obj,key,{
			get(){//取值触发
				return value
			},
			set:(newVal)=>{//赋值触发
				// 当赋的值和老值一样,就不重新赋值
				if (newVal != value) {
                	this.observer(newVal)//新值,做成响应式
					value = newVal 
					//执行watcher中的update方法
					dep.notify()
				}
			}
		})
	}
}

// vue中实例化
class Vue{
	constructor(options){
		this.$el = options.el || '#app'
		this.$data = options.data() || {}
		this.$computed = options.computed || {}
		this.$watch = options.watch || {}
		this.$methods = options.methods || {}
		new Observer(this.$data) //添加
	}
}


给data添加代理

实现通过this.访问data上数据,同样也是巧妙利用Object.defineProperty方法

class Vue{
	constructor(options){
		this.$el = options.el || '#app'
		this.$data = options.data() || {}
		this.$computed = options.computed || {}
		this.$watch = options.watch || {}
		this.$methods = options.methods || {}
		this._proxyData(this.$data)
		new Observer(this.$data)
	}
	_proxyData(data){
		Object.keys(data).forEach((key)=>{
			Object.defineProperty(this,key,{
				get:()=>{
					return data[key]
				}
			})
		})
	}
}

2.Compiler实现

原理:

  1. 先获取模板el所有元素,遍历放入文档碎片fragment中 (添加node2fragment函数)
//先定义工具函数,后面处理不同的指令
const CompilerUtil = {

}
//记得在Vue中new Compiler(this.$el, this)
class Compiler{
	constructor(el,vm){
		this.vm = vm
		this.el = this.isElementNode(el) ? el : document.querySelector(el)
		let fragment = this.node2fragment(this.el)
	}
	node2fragment(node){
		let fragment = document.createDocumentFragment() // 创建文档碎片
		let firstChild
		while (firstChild = node.firstChild) { //当node存在子节点,并取出放入firstChild
			fragment.appendChild(firstChild) //将取出的firstChild,放入fragment
		}
		return fragment //最后返回完整剪切下来的fragment
	}
	//判断一个节点是否是元素节点
	isElementNode(node){
		return node.nodeType === 1
	}
}

  1. 编译已经放入文档碎片上的模板数据

在Compiler类上定义compile函数,传入模板数据,遍历节点列表,分别处理元素节点和文本节点

	compile(node){
		let childNodes = node.childNodes;   //节点列表
		[...childNodes].forEach((child)=>{
			//判断是元素节点,还是文本节点
			if(this.isElementNode(child)){
				this.compileElement(child) //编译元素节点
				this.compile(child) // 递归遍历子节点
			}else{
				this.compileText(child) //编译文本节点
			}
		})
	}

2.1 在Compiler类上定义compileElement函数,传入元素节点,遍历其属性节点,调用工具函数处理对应的指令

compileElement(node){
    let attributes = node.attributes;    //某个元素的属性节点
    [...attributes].forEach((attr)=>{
        let { name, value: expr } = attr
        //判断是否是指令
        if(this.isDirective(name)){
            let [directive, value] = name.substring(2).split(':')
            CompilerUtil[directive]&&CompilerUtil[directive](node, expr, this.vm)//调用不同的处理指令
        }
    })

}
//判断是否是指令
isDirective(isDirective){
    return attrName.startsWith('v-')
}

2.2 在Compiler类上定义compileText函数,传入文本节点,再调用工具函数处理对应的指令

compileText(node){
    let content = node.textContent
    let reg = /\{\{(.+?)\}\}/;  //得到插值表达式,也就是 {{ }}
    if (reg.test(content)) {
        //找到文本节点
        // content  {{school.name}}  {{school.age}}
        CompilerUtil['setTextContent'](node, content, this.vm)
    }
}

2.3 实现工具函数CompilerUtil对象,根据表达式获取data上的值,并设置到节点上

const CompilerUtil = {
	//处理v-text指令
	text(node,expr,vm,){
		let fn = this.updater['textUpdater']
		let content = this.getVal(vm, expr)
		fn(node, content)
	},
    //处理{{student.name}}等表达式
	setTextContent(node,expr,vm) {
		let fn = this.updater['textUpdater']
		let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
			return this.getVal(vm, args[1])
		})
		fn(node, content)
	},
    //获取data上对应的值
	getVal(vm,expr){
		return expr.split('.').reduce((data,cur)=>{
			return data[cur]
		},vm.$data)
	},
	updater:{
		textUpdater(node,value){
			node.textContent = value
		}
	}
}

2.4 将编译好的文档碎放入真是节点this.el中

class Compiler{
	constructor(el,vm){
		this.vm = vm
		this.el = this.isElementNode(el) ? el : document.querySelector(el)
		let fragment = this.node2fragment(this.el)
		//编译模板数据
		this.compile(fragment)
		this.el.appendChild(fragment) //添加
	}
    ...
}

至此就已经实现将数据展示到了视图上面。