前言
本文主要帮助理解 vue前端框架做了什么 ,顺着vue响应式功能结果逆推主要功能简单实现。目标:手写 Vue 2.x 的简单响应式实现
1. 数据响应式核心原理
首先 了解一下核心API:Vue2.x Object.defineProperty() | Vue3.x Proxy()
vue的数据响应式核心实现 在2.x版本使用Object.defineProperty() 和3.x版本更新使用Proxy() ;
数据响应式:数据模型仅仅是普通的js对象,当我们修改数据时候,视图进行更新,避免繁琐 DOM操作
双向绑定: MVVM模式 数据改变 视图改变; 视图改变 数据也随之改变 v-model在表单元素上创建双向数据绑定
数据驱动是vue的特性之一:仅仅关注数据本身 不需要关心数据如何渲染到视图的 vue背后都自己做了
1.1 数据劫持:Object.defineProperty(obj, prop, descriptor)
引用MDN介绍该API: Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
参数:
-
obj要定义属性的对象。
-
prop要定义或修改的属性的名称或
Symbol。 -
descriptor要定义或修改的属性描述符。
返回值:
被传递给函数的对象。
object.defineProperty(),里面的get set方法可以劫持对象数据 并修改对象数据 返回这个对象
let data = {
msg:'hello',
count:19
}
//vue实例
let vm ={}
//数据劫持 vue2 defineProperty
//Object.keys() 遍历对象属性 并给对象的所有属性添加get set 方法 也就是:当你在外部代码处使用到这个数据的时候data[key]就会触发这个get方法 ,当你改变这个数据的时候 this.xxx = sss 就会触发 set方法。
//这就是数据劫持 ,可以看出 这个get和set 是个很好的节点 在get set这里 可以捕捉到最新变化 比如: 数据变化之后 去更新数据修改视图 document.querySelector("#app").textContent= data[keys]
Object.keys(data).forEach((keys)=>{
Object.defineProperty(vm,keys,{
configurable:true,
enumerable:true,
get(){
console.log("getter",data[keys]);
return keys
},
set(newValue){
data[keys]=newValue;
console.log("setter",newValue);
//当数据改变之后 更新数据渲染到视图上
document.querySelector("#app").textContent= data[keys]
}
})
vm.msg="hello World"
vm.count="20"
console.log(vm.msg, vm.count);
})
1.2 对象代理: Proxy()
MDN:Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
const p = new Proxy(target, handler)
参数
-
target要使用
Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。 -
handler一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理
p的行为。
get 两个参数 target 包装目标对象 和key 目标属性
set 三个参数 newValue 目标属性改变的新值
let vm = new Proxy(data,{
get(target,key){
console.log("getter!",target[key]);
return target[key]
},
set(target,key,newValue){
target[key]=newValue;
console.log("setter", target[key]);
document.querySelector("#app").textContent= target[key]
}
})
2.发布订阅者模式
某个任务完成,发布者向事件中心发布信号,其他任务可以向事件中心订阅这个信号,从而知道什么时候可以执行任务
Vue中的例子 自定义事件:EventBus emit
EventBus :事件触发器(事件中心)
$on :注册事件 (发布者)
$emit: 触发事件 (订阅者)
// 简写一下 vue中的发布订阅模式
//事件中心 是一个对象 存储注册的事件
class EventEmitter {
constructor(){
//构造函数 创建一个对象
//{onchange:[fn1,fn2],click:[fn3] }
this.subs=Object.create(null)
}
// 两个参数 事件名字 和事件函数
$on(eventType,handler){
this.subs[eventType] = this.subs[eventType] || []
// this.subs是对象 对象里的每一项是一个数组
this.subs[eventType].push(handler)
}
//一个参数 事件名字 拿到事件中心已经注册的事件 调用它
$emit(eventType){
this.subs[eventType].forEach(handler => {
handler()
});
}
}
let em= new EventEmitter()
//注册事件
em.$on('onchange',()=> {
console.log("on1....");
})
em.$on('onchange',()=> {
console.log("on2....");
})
//触发事件
em.$emit('onchange')
3.观察者模式
观察者没有事件中心 只有 发布者-目标、 订阅者-观察者
其中 发布者有所有的观察者,改变后通知观察者。 观察者就调用update方法 订阅事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>观察者模式</title>
</head>
<body>
<div id="app">
hell0
</div>
</body>
<script ></script>
<script>
//发布者
class Dep {
//添加所有的观察者
constructor(){
this.subs=[]
}
// 将观察者添加
addSub(sub){
if(sub && sub.update){
this.subs.push(sub)
}
}
notify(){
this.subs.forEach(sub=>{
sub.update()
})
}
}
//观察者(订阅者)
class Watcher {
update(){
console.log("dddddddd");
}
}
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
//发布者会将观察者添加到自己数组列表中,然后当数据改变之后 发布通知,(notify) 观察者就会更新自己的update方法
</script>
</html>
4.简写Vue
简化成 核心代码 vue.js observe.js compiler.js dep.js watch.js 有这五个
文件整体目录:
4.1 vue.js
Vue:
负责接收初始化的参数(data)
负责把data中的属性注入到vue实例中,转换成getter setter
负责调用observer 监听data中所有属性的变化
负责调用compiler 解析指令\差值表达式
vue.js
class Vue {
constructor(options){
//初始化的参数
this.$options=options;
this.$data = options.data;
this.$el = typeof options.el ==='string'?document.querySelector(options.el):options.el;
//注入到vue 实例 并转换getter setter
this._proxyData(this.$data)
// 在 创建observer 再考虑调用observe对象 监听数据变化
//new Observe(this.$data)
// 在 Compiler 再考虑 调用compiler对象 解析指令和差值表达式
//new Compiler (this.$el)
}
//这里是把data 注入到vue实例上
_proxyData(data){
Object.keys(data).forEach(key=>{
Object.defineProperty(this,key,{
configurable:true,
enumerable:true,
get(){
return key
},
set(newValue){
if(newValue===data[key]){
return
}
data[key]=newValue
}
})
})
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini Vue</title>
</head>
<script src="./js/vue.js"></script>
<body>
<div id="app">
<h1>差值表达式</h1>
<h3>{{ msg }}</h3>
<h3>{{ count }}</h3>
<h1>v-text</h1>
<div v-text="msg"></div>
<h1>v-model</h1>
<input type="text" v-model="msg"></input>
<input type="text" v-model="count"></input>
</div>
</body>
<script>
let vm = new Vue({
el:"#app",
data:{
msg:"hello world",
count :1011,
person:{
name:"zzws"
}
}
})
console.log(vm)
//打印vm 可以查看已经注入到vue中 也转换了get set
</script>
</html>
4.2 observer.js
Observer:
负责把data选项中的属性 转换成响应式数据 defineReactuve
data中的属性是对象的话 也把属性转换成响应式数据
数据变化发送通知 (这里是用到dep这个对象 后续细表)
observe:
walk(data)
defineReactuve(data,key,value)
observe.js
class Observer{
constructor(data){
this.walk(data)
}
walk(data){
if(typeof data !=='object' || !data){
return
}
//遍历所有属性
Object.keys(data).forEach(key=>{
//这里为什么要穿data[key] 已经有了 前两个参数 是因为到Object.defineProperty get方法的时候 我们使用data[key] 时 就相当于又使用了get方法 就会陷入栈溢出 陷入死循环
this.defineReactive(data,key,data[key])
})
}
defineReactive(data,key,value){
//this 指向问题需要注意
let that = this
// value 是对象 把内部属性转换成响应式数据
this.walk(data)
Object.defineProperty(data,key,{
get(){
//这里如果使用data[key] 不信你试试
return value
},
set(newValue){
if(newValue===value){
rerurn
}
value=newValue
// 改变后的新值如果是对象 把内部属性也转换成响应式数据
that.walk(newValue)
//改变了就发送通知
}
})
}
}
不要忘记 html中引入 和 vue.js中创建 new Observer对象
4.3 compiler.js
Compiler :
负责编译模版 解析指令 差值表达式
负责页面的首次渲染
当数据变化后 重新渲染视图
el vm compile(el) compileElement(node) compileText(node)
isDirective(attrName) isTextNode(node) isElementNode(node)
compiler.js
class Compiler {
constructor(vm){
this.$el = vm.el;
this.vm = vm
//立即调用编译模版
this.compile(this.$el)
}
//编译模版 处理指令和差值表达式
compile(el){
let childNodes = el.childNodes //伪数组
Array.from(childNodes).forEach(node=>{
if(this.isTextNode(node)){
this.compileText(node)
}else if(this.isElementNode(node)){
this.compileElement(node)
}
//递归调用子节点 如果子节点下面还有节点
if(node.childNodes&&childNodes.length){
this.compile(node)
}
})
}
//编译元素节点 解析指令
compileElement(node){
//通过属性节点 可以找到属性名称和属性值
// console.log(node.attributes)
//遍历所有属性节点
Array.from(node.attributes).forEach(attr=>{
let attrName = attr.name
// 判断是否是指定 是哪个指令 v-type 还是v-modle
if(this.isDirective(attrName)){
//v-text 变成 text
attrName = attrName.substr(2)
let key = attr.value
console.log(key);
this.update(node,key,attrName)
}
})
}
update(node,key,attrName){
let updateFn = this[attrName + 'Updater']
console.log(this.vm);
updateFn && updateFn(node,this.vm[key])
}
//处理v-text 指令
textUpdater(node,value){
node.textContent = value
}
//处理v-model 指令
modelUpdater(node,value){
node.value = value
}
//编译文本节点 处理差值表达式
compileText(node){
console.dir(ndoe)
// console.dir(node) .匹配任意字符 匹配到差值表达式
// {{ msg }}
let reg = /{{(.+?)}}/
let value = node.textContent
if(reg.test(value)){
let key = RegExp.$1.trim()//拿到msg
console.log(key);
node.textContent = value.replace(reg,this.vm[key])
}
}
//判断元素属性 是不是指令
isDirective(attrName){
return attrName.startsWith('v-')
}
//是否是文本节点
isTextNode(node){
return node.nodeType==3
}
//是否是元素节点
isElementNode(node){
return node.nodeType==1
}
}
4.4 dep.js
dep.js
发布者 Dep 当Observer监听数据变化,把data数据变成get set dep的作用就是在 get的时候收集依赖,(每一个响应式属性都会创建一个dep对象,收集依赖于该属性的地方,所有依赖于该属性的地方,都会创建一个watcher对象,所以dep都会收集依赖于该属性的watcher对象 。 在set方法中通知依赖,当属性发生变化时,dep.notify发送通知 调用watcher的update方法。
get中收集观察者 set中通知观察者
class Dep {
constructor(){
this.subs = []//保存sub
}
addSub(sub){
//添加sub
if(sub && sub.update){
this.subs.push(sub)
}
}
notify(){
每个订阅者都要更新方法
this.subs.forEach(sub=>{
sub.update()
})
}
}
再在observer里 添加依赖 和通知
defineReactive(obj,key,value){
let that = this
let dep = new Dep()
// value 是对象 把内部属性转换成响应式数据
that.walk(value)
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
//检查Dep类的target存不存在 这个target是保存watcher对象的 在watcher里面保存
//存在就添加进dep数组里保存依赖 等到数据更新的时候在set里通知到订阅者watcher
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue){
if(newValue===value){
return
}
value=newValue
that.walk(newValue)
// 发送通知 数据更新了就调用notify方法通知 会调用watcher的update方法
dep.notify()
}
})
}
注意 在index.html里五个js文件的引入关系
4.5 watcher.js
watch.js
当数据变化触发依赖 dep通知所有的watcher 实例更新视图
自身实例化的时候往dep对象中添加自己
watcher.js
class Watcher{
constructor(vm,key,cb){
this.vm = vm
this.key = key
this.cb = cb
//把watcher对象记录到dep类的静态属性target方法
Dep.target = this
//触发get方法,在get方法中调用addSub
this.oldValue = vm[key]
Dep.target = null
}
update(){
//更新方法 会调用回调函数更新数据
let newValue = this.vm[this.key]
if(this.oldValue === newValue){
return
}
this.cb(newValue)
}
}
代码中 所有依赖于数据的地方都要创建一个watcher对象 当数据改变的时候 dep对象 会通知watcher对象 去渲染视图
compiler.js
class Compiler {
constructor(vm){
this.el = vm.$el;
this.vm = vm
//立即调用编译模版
this.compile(this.el)
}
//编译模版 处理指令和差值表达式
compile(el){
let childNodes = el.childNodes //伪数组
Array.from(childNodes).forEach(node=>{
if(this.isTextNode(node)){
this.compileText(node)
}else if(this.isElementNode(node)){
this.compileElement(node)
}
//递归调用子节点 如果子节点下面还有节点
if(node.childNodes&&childNodes.length){
this.compile(node)
}
})
}
//编译元素节点 解析指令
compileElement(node){
//通过属性节点 可以找到属性名称和属性值
console.log(node.attributes)
//遍历所有属性节点
Array.from(node.attributes).forEach(attr=>{
let attrName = attr.name
// 判断是否是指定 是哪个指令 v-type 还是v-modle
if(this.isDirective(attrName)){
//v-text 变成 text
attrName = attrName.substr(2)
let key = attr.value
console.log(key);
this.update(node,key,attrName)
}
})
}
update(node,key,attrName){
let updateFn = this[attrName + 'Updater']
console.log(this);
//这里使用call 改变this指向
updateFn && updateFn.call(this,node,this.vm[key],key)
}
//处理v-text 指令
textUpdater(node,value,key){
console.log(this);
node.textContent = value
//创建watcher对象 当数据改变 渲染视图 这里的this是compiler类 所以要上面updatFn函数改变this指向 回调函数传改变的值进去
new Watcher(this.vm,key,(newValue)=>{
node.textContent = newValue
})
}
//处理v-model 指令
modelUpdater(node,value,key){
node.value = value
//创建watcher对象 当数据改变 渲染视图
new Watcher(this.vm,key,(newValue)=>{
node.value = newValue
})
// 双向绑定 当input框值改变时 更新数据
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
//编译文本节点 处理差值表达式
compileText(node){
// console.dir(node) .匹配任意字符 匹配到差值表达式
// {{ msg }}
let reg = /{{(.+?)}}/
let value = node.textContent
if(reg.test(value)){
let key = RegExp.$1.trim()//拿到msg
// console.log(key);
node.textContent = value.replace(reg,this.vm[key])
//创建watcher对象 当数据改变 渲染视图
new Watcher(this.vm,key,(newValue)=>{
node.textContent = newValue
})
}
}
//判断元素属性 是不是指令
isDirective(attrName){
return attrName.startsWith('v-')
}
//是否是文本节点
isTextNode(node){
return node.nodeType === 3
}
//是否是元素节点
isElementNode(node){
return node.nodeType === 1
}
}
简单的vue的双向数据绑定 完成.