手写MVVM的实现原理
Posted 十九万里
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写MVVM的实现原理相关的知识,希望对你有一定的参考价值。
一、学习目标
了解object,definProperty vue2.0使用都是这个响应式的监听
手写并优化MVVM底层实现 实现v-text和v-model原理
二、 数据响应式
1数据响应式是什么
数据发生变化,我们可以马上知道,并且做一些事情,关键在于知道数据发生变化并做出对应的操作
2、如何实现一个数据响应 及区别
vue2.0 对象属性拦截:Object.defineProperty (原生js的方法) 全局拦截
vue3.0版本中:对象属性拦截 Proxy对象代理 部分拦截
两者实现响应式的原理是一样的都是javascript的原生写法
区别:
vue2.0的配置项中,只要放到date中的数据,不管有没有用到或者数据有多深入,vue都会对这个数据进行递归处理。所以尽量不要放很多嵌套深的数据
vue3.0中的proxy 劫持对象整体,惰性处理,只有在data中的数据被 用到的时候,才会进行处理 减少了无端性能的消耗。
代码实现:
// 定义对象方式:
// 1、字面量定义
let data = {
name:"pc"
}
// 如果修改name属性 其实并不知道name属性发生了变化
// 三个参数:定义的对象 声明的属性 page对象对于当前属性的精细化对象 里面主要注意get和set函数
Object.definePerty(data,'name',{
// get属性就是当我们访问data1的name属性是后自动调用的方法 get函数的返回值就是拿到的值
get(){
console.log ('访问到data1中的值')
return "pc"
},
// 当我们修改设置name属性的时候,会自动调用函数 属性最新的值会被当成实参传进来 这个newValue就是最新的值
set (newValue ){
consloe.log('修改了data1中的值,newValue就是修改之后最新的值,被定义为一个新的传参')
}
})
// 访问的时候会调取get方法 修改的时候会调取set方法并返回一个传参 可以在访问属性和设置属性的时候调用对应的函数
// 注:set方法的特点是一旦修改某个属性就会立即执行 数据更新就会执行set函数 可以在函数里面添加新的动作。
// 如果想在数据变化的时候完成一些自己的事情 都可以放到set函数里面执行。
// 例如 ajax() 操作一些dom区域,
3、优化get和set联动
// 优化就是在使用set修改之后 第二次使用get时拿到就是修改后的值
// 没有做修改的话第二次修改就还是之前的第一次原有的值 所以要进行优化联动 set和get函数原始的并未发现联动问题
// 联动思路 是因为我们的代码中set和get中没有任何的关联 需要修改get中返回的值应该改为动态的。
// 解决方法: 定义中间变量 _name 把修改后返回的参数newValue赋值给 _name 这样get在执行的时候放回的就是最新修改的newValue 这样的话set和get操作的就是一个数据了
let data = {}
let _name = 'pc'
Object.definePerty(data,'name',{
get(){
console.log ('访问到data1中的值')
return _name
},
set (newValue ){
consloe.log('修改了data1中的值,newValue就是修改之后最新的值,被定义为一个新的传参',newValue)
_name = newValue
}
})
4 、优化对象数据劫持与转化
在使用vue开发的过程中 ,都是提前把响应式数据放到data配置中,一般来说 响应式数据有很多个,对象的属性很多,不止一个,上面写的对象只有一个,不太方便,我们现在要做的事情就是如何把提前声明好的对象,要把所有 属性编程set和get的形式,目标就是访问定义好的data中的属性和改变。初步想法是遍历
let data = {
name = 'pc',
age = '16',
height = '177'
}
// 1、由于有多个属性需要进行对象的遍历
// key可以代表data对象的每一个属性名
// data[key]代表data对象每一个属性队形的value值 其实就是键值对的关系,data 源对象。Object.keys(data)是对象的遍历方法 得到一个数组所以使用数据的forEach方法来遍历。
// 处理每一个对象的key 转变为响应式 这样的话就每一个属性都进行了set和get的转化
Object.keys(data).forEach(key)=>{
console.log(key,data[key])
observe(data,key,data[key])
}
// data 要处理的对象
// 可以要处理的对象属性名 (键)
// value 要处理的对象属性名对应的初始值 (值)
// 2、把对象的属性转化为get和set的形式 让后调用
function observe(data,key,value){
Object.defineProperty(data,name,{
get(){
return value
},
set (newValue){
value = newValue
}
})
}
4、扩展理解作用域闭包的独立性
每一个函数作用域希望是一个独立的功能,
总结:1、尽量不要在一个函数中写大量的逻辑,而是通过传参的形式来分成多个函数。
2、属性遍历时,每遍历一次调用一次definReactive函数,形成多个独立的函数作用域,每一个函数作用域中 get和set中的value都是独立的,互不影响。
三、数据的变化反应到视图
本质上还是操作dom
1、命令式操作视图
命令式的操作视图就是手动操作
// 1.准备数据
let data = {
name = 'pc'
}
// 2、把数据转化成为响应式
Object.keys(data).forEach(key)=>{
observe(data,key,data[key])
}
function observe(data,key,value){
Object.defineProperty(data,name,{
get(){
console.log('访问l数据',key)
return value
},
set (newValue){
// 优化性能 主动判断是否修改了值 需要主动去判断。如果没有修改直接return出去
if (newValue === value){
return
}
console.log('修改了数据',key)
value = newValue
// 把最新的值放到视图中 这里是关键的位置 核心是操作dom api 把最新的值操作上去,
// html部分我就不写拿到app b并调用innerText把最新的值赋值上去,只要修改name属性就会修改最新的值。
document.querySelector('#app p').innerText = newValue
}
})
}
2、声明事操作视图
实现v-text 声明式的指令版本
目标:一但data中的那么发生变化后 标记的v-text的文本内容就会立即得到更新
实现指令的核心:不管是指令也好还是插值表达式也好,他们都是数据和视图之间建立的关联的标志
所以本质上就是通过一定的手段找到符合表示的dom元素,然后把数据放上去 数据改变的时候重新执行一次dom操作
<body id = "app">
<p v-text="name">
</p>
</body>
<script>
let data = {
name :'pc'
}
// 实现步骤:
// 1.先通过标识查找把数据放到对应的dom中显示出来
function compile(){
let app = document.getElemetById('app')
xonst childNodes = app.childNodes // 拿到的所有节点 包括文本节点和标签节点
console.log(chilNodes)
// 筛选出来目标节点 去除没用的文本节点。
chilidNodes.forEach(node=>{
if(node.nodeType === 1){
// 表示这里拿到的是p标签节点
// 筛选v-text属性,标签上可能有p id v-text等属性 我们只要需要的 使用属性attributes拿到对应的标签
// 拿到所有的标签属性
cosnt =attrs = node.attributes
// 遍历所有的标签并判断 Array.from是es6中的方法,可以把类数组转化成为数组
Array.from(attrs).forEach(attr=>{
//这里拿到的其实是一个对象 包括键和值
const nodeName = attr.nodeName // 对象名 (键 我们需要找到v-text
const nodeValue = attr.nodeValue // 对象值 (值 其实就是data中对应的key
// 把date中的数据放到满足标识的dom上
if(nodeName === 'v-text'){
node.innertText = data[nodeValue]
}
})
}
})
}
compile()
// 2.数据变化之后在此执行将数据放到最新的dom上
</script>
62b6d7264a315cb3
T0014
3、总结
四、发布订阅模式实现
浏览器内部的实现过程分析
1、浏览器实现了一个方法叫做addEventListener
2、这个方法接受了两个参数,参数1代表事件类型,参数二代表回调函数
3、为了实现一对多的架构 在内部绑定了多个回调函数数组。
4、当鼠标点击的时候 通过事件类型click去数据结构中去找到存放所有相关回调函数的数组并遍历,都执行一遍 从而实现一对多
实现一个自己的自定义事件收集和触发机构
//1 。定义一个方法接受两个参数参数一为事件类型 参数2位回调函数 只要方法一执行 就收集回调函数到对应的位置上去,几个参数的含义:【eventName:事件名称;fn :回调函数
const map ={
}
function collect(eventName,fn){
// 如果当前map中已经初始化好了click:[]
//就直接往里面push,如果没有初始化 就先进行初始化。
if(!map[eventName]){
map[eventName] = []
}
map[eventName].push(fn)
}
collect('cp',()=>{
console.log("收集成功了cp")
})
collect('cp',()=>{
console.log("第二次收集成功了cp")
})
console.log(map)
//以上完成了事件的收集操作,实现了一对多的存储架构
//上面事件收集好了 下面需要模拟鼠标点击,注定去通过程序触发收集起来的事件。需要通过事件名称 找到对应的回调函数 然后遍历执行即可
function trigger(eventName){
map[eventName].forEach(fn => fn())
}
trigger('cp')
//上面的函数中 事件函数都暴露在全局作用域中,优化是把所有的事件相关的数据结构添加到一个对象中去
const Dep = {
map:{},
//收集事件的方法
collect(eventName,fn){
if(!map[eventName]){
map[eventName] = []
}
map[eventName].push(fn)
}
},
trigger(eventName){
this.map [eventNmae].forEach(fn => fn())
}
五、总结
1、Object.defineProperty ES6中原生的方法
object.defineProperty(data,'name',{
get(){
return "查看的值"
}
set(newValue){
// 设置之后最新的值作为参数返回。
// 可以在set函数里做一些操作 ajax 操作dom等等
}
})
2、数据反应到视图
数据的变化可以引起视图的变化(通过操作dom你数据放到对应的位置上去,如果数据变化之后就用最新的数据再重新做一遍)
方案一 :命令式操作视图
1、documnet.querySelector('#app p ').innerText = data.name
2、set 函数中在执行一遍documnet.querySelector('#app p ').innerText = data.name
方案二:声明式渲染 (v-text方式的实现)
核心逻辑:通过“模板编译”找到标记了v-text的元素,然后把对应的数据通过操作dom的方式放上去
1、通过app根元素找到所有的子节点(元素节点 文本节点) dom.nodeChilds
2、通过节点类型筛选出元素节点 nodeType 1元素节点 3文本节点
3、通过v-text找到需要设置的具体节点
4、找到绑定了v-text的标记元素,拿到他身上所有的属性
5、通过v-text=“name”拿到指令类型“v-text” 拿到需要绑定的数据属性名’name‘
6、判断当前是v-text指令 然后通过操作domapi 把name属性对应的值放上去,node.innerText = data[name]
v-model原理和v-text一样 需要改变的是56 除了指令类型和domapi不一样 其他的的都是一致的。
5、通过v-model= “name”拿到v-model拿到所需要绑定的数据属性名‘name’
6、判断当前是v-model指令,然后通过操作DOMapi 把name属性对应的值放上去 node.value = data[name]
3、 V -> M
本质:事件监听在回到函数中拿到哦input中输入的最新值,然后赋值给绑定的属性
node.addEventListener('input',(e)=>{
data[name] = e.target.vaule
})
4、优化工作
//1、通用的数据响应式处理
data(){
return{
name:'pc',
age: 23
}
}
//基于现成的数据,处理成响应式
Object.keys(data)//由所有对象的key组成的数组
Object.keys(data).forEach(key=>{
//key 属性名
// data[key]属性值
//data 原对象
//将所有的key都转化为get和set的形式所以需要定义一个 defineReactive函数,传回三个参数
defineReactive(data,key,data[key])
})
function defineReactive(data,key,value){
object.defineProperty(data,key,{
get(){
return value
}
set(newValue){
value = newValue
}
})
}
2、发布订阅模式
/问题:
<div>
<p v-text="name"> </p>
<p v-text="name"> </p>
<div v-text="age"> </div>
</div>
name发生变化之后,我们需要做的是更新属性为name的标签,现在的情况是无论哪个属性发生了变化,所有的标签都会跟着变化。无法做到准确更新
解决问题的思路:
1、数据发生变化之后 最关键的代码:node.innerText = data[name]
2/设计一个存储结构
每一个响应式数据可能被多个标签绑定 是一种一对多的关系
{
name :[()=>{ndoe(p1)innerText = data[name],()=>{ndoe(p2)innerText = data[name]...]
}
发布订阅模式 解决的是我们一对多的问题
实现简单的发布订阅模式:浏览器事件模型
dom.addEventListener('click',()=>{})
只要调用click事件 所有绑定的回调函数都会执行 其实就是有一对多的关系
const Dep = {
map:{},
collect(evnetName,fn){
// 如果没有调用过事件先初始化成数组
if(!this.map[eventName]){
this.map[eventName] = []
}
// 已经调用过事件 直接往里面push添加数组
this.map[eventName].push(fn)
}
//trigger函数是用来触发事件
trigger(eventName){
this.map[eventName].forEach(fn =>fn())
}
}
//使用发布订阅优化现存问题
// 之前的写法不管是哪个数据发生变化 我们都是粗暴的执行一下complie函数即可
// 现在的写法 我们在complie函数储值执行的时候 完成更新函数的收集,然后再数据变化的时候通过数据的key找到对应的更新函数 依次执行达到精准更新的效果。
以上是关于手写MVVM的实现原理的主要内容,如果未能解决你的问题,请参考以下文章