从零开始一步一步实现个简易版vue,包含响应式,computed,watch,methods等原理。
MVVM原理
Vue2.0 响应式原理最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的;
关于Vue响应式机制,Object.defineProperty()之前的文章 vue 响应式机制简述 已经介绍过了
而要实现Vue,需要从以下几步开始:
- 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
- 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
- 实现一个解析器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实现
原理:
- 先获取模板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
}
}
- 编译已经放入文档碎片上的模板数据
在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) //添加
}
...
}
至此就已经实现将数据展示到了视图上面。