1.在学习Vue的过程中。发现后天的webpack都是配置,想提高开发效率,所以中断了Vue的学习进程(有兴趣的时候再去看吧)
2.这两天也是隔离的最后两天。于是3分热度的又想探索Vue是怎么实现双向绑定的。所以花了点时间学习了Vue MVVM的实现简单实现过程。https://www.bilibili.com/video/av54530725?t=2678&p=2
3.发现这个老师挺牛逼。手动实现,思路也挺清晰。
4.个人理解能力不强,看到第二个课程的时候跟不上了。没怎么明白是怎么实现双向绑定的。于是傻乎乎的跟着敲完实现了同样的功能。
5.我还是想弄清楚是一个怎么样的调用过程。于是乎花了几个小时的时间进行剖析代码执行
总的来说,
1.修改data中的对象声明方式。使用definepropery这个函数进行重新定义。使用这个函数的好处是,当数据发生改变的时候会触发get,set方法
Object.defineProperty(obj,key,{
get(){
//创建watcher时,会取到对应的target内容
//取的时候 存放这个watcher
Dep.target && dep.addSub(Dep.target)
return value;
},set:
(newVal)=>{ //{{persson.name}} = {}
if(value != newVal){
this.observer(newVal) //所以要监控/防止 person ={a:1}被后台改了
value = newVal;
//设置的时候通知
dep.notify()
}
}
})
2.进行编译(让页面中使用Vue标签的展现为正常内容)
- 添加watcher
- 添加事件(input)
- 替换html页面中展现的Vue标签-触发当前对象的set方法(Dep notify当前对象的观察者(watcher))
其他实现的一些内容还得重新理解理解。才能够自己动手的写出来
源码
Compiler.js
class Compiler{
constructor(el,vm){
//元素 和Vue实例
//el可能是字符串或者是document.getElementById("#app")
//获取当前 元素---第一层咯,孩子和孙子还没加载到内存里面
this.el = this.isElementNode(el)?el:document.querySelector(el);
this.vm = vm; //Vue实例
//console.log(this.el)
//将当前的模板替换 v-xxx {{}}
//将这些元素放在"内存"里面 文档碎片//替换完毕后再返回来
let fragment = this.node2fragment(this.el);
//console.log(fragment)
//将节点中的元素进行替换
//编译模板 attention
this.compile(fragment)//用vm去编译 文档碎片
///
//再把节点赛道页面中---元素添加回去
this.el.appendChild(fragment)
}
//编译文档碎片
compile(node){
//v-xxx 和 {{}}
let childNodes = node.childNodes;//第一层节点,不包含孙子节点 (空文本)
//console.log(childNodes)
//类数组---》数组
//let childNodesArray = this.classArray2Array(childNodes)
;[...childNodes].forEach(child=>{
//childNodesArray.forEach(child=>{
//不是元素就是 空文本
if(this.isElementNode(child)){
//元素是否有v-???
//console.log('element')
this.compilerElement(child)
this.compile(child)//子元素可能还有子元素
}else{
//文本则看是否有{{}}
//console.log('text')
this.compilerText(child)
}
})
}
/
//判断是否v- 开头
isDirective(attrName){
return attrName.startsWith("v-")//v-开头
}
//万有引力
//编译元素 是否有v-???
compilerElement(node){
//可能还有子元素
let arrtributes = node.attributes
;[...arrtributes].forEach(attr=>{
//name = value
//type="text"
//v-model="person.name"
let {name,value} = attr
//判断名字是否是v-model 是不是v-开头的
if(this.isDirective(name)){
//console.log(name);//v-model
let [,directive] = name.split("-")//model 调用方法model
let [directiveName,eventName] = directive.split(":")//on click
//执行方法,去替换node中的内容,需要的内容从vm里面获得
// node person.name vm实例 事件名字click
// 当前页面节点。绑定的表达式 vm实例(可以获得表达式的值) 事件名
CompileUtil[directiveName](node,value, this.vm, eventName)//调用不同的指令来处理
}
})
}
//编译文本 是否有{{}}
compilerText(node){
let content = node.textContent;
//匹配{{xxxx}}
if(/\{\{(.+)\}\}/.test(content)){
//console.log(content,'text')
CompileUtil['text'](node,content,this.vm)
}
}
/
//由于语法不支持,自己写一个函数,类数组---》数组
//已经可以了,类数组前面加上;[...Array]
classArray2Array(classArray){
let array = []
for(let i=0,len=classArray.length;i<len;i++){
array.push(classArray[i])
}
return array
}
//将node节点放在内存里面
node2fragment(node){
let fragment = document.createDocumentFragment();
let firstChild
while(firstChild = node.firstChild){
//appendChild每拿到一个结点就从原来的位置删除
fragment.appendChild(firstChild)//
}
return fragment;
}
//是不是元素
isElementNode(node){
return node.nodeType === 1;
}
}
CompileUtil.js
CompileUtil = {
getValue(vm,expr){//vm.$data 'person.name'.
return expr.split('.').reduce((data,current)=>{
return data[current]//成为下一个的第一个参数
},vm.$data)//初始化值
},
setValue(vm,expr,value){//person.name
//通过输入事件调用这个方法
expr.split('.').reduce((data,current,index,arr)=>{
debugger
if(index == arr.length-1){//最后一次
return data[current] = value//调用set
}
return data[current]//调用get
},vm.$data)//初始化值
},
model(node,expr,vm){
//页面节点 绑定的表达式 person.name vm实例---》可以通过vm实例获得表达式的值
//node是节点,expr是表达式,vm对象
//给输入框赋予value值 nade.value = xxx
//modelUpdate(node,value){node.value = value}
let fn = this.updater['modelUpdate']
//
//给输入框加上一个观察者,等会数据更新了 就触发方法
//会拿新值 给输入框服务值
//第二课加 数据一更新 就调用回调方法。。在观察者里面定义的。
//数据一更新,视图调用这个方法
//添加监控。
//node 闭包,
new Watcher(vm,expr,(newValue)=>{
fn(node,newValue)//进行更新 //node.value = newValue
})
node.addEventListener('input',(e)=>{
let value = e.target.value//获取当前输入的值
//给当前的表达式设置新的值
this.setValue(vm,expr,value);//ode.value = value
})
//
let value = this.getValue(vm,expr)
fn(node,value)//进行更新
},
html(node,expr,vm){//v-html="message"
let fn = this.updater['htmlUpdater'] //修改内容
new Watcher(vm,expr,(newValue)=>{
fn(node,newValue)//进行更新
})
//
let value = this.getValue(vm,expr);
fn(node,value)//进行更新
},
on(node,expr,vm,eventName){//v-on:click = "change" //点击的时候,执行自己定义的change方法
//change
debugger
// click
node.addEventListener(eventName,(e)=>{
vm[expr].call(vm,e)///this.change这个方法是没有的
//vm实例.
//vm[person.name]
})
},
getContentValue(vm,expr){
//遍历表达式 将内容 重新替换一个完整的内容
// +? 重复1次以上,说明前面的 .
//
return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getValue(vm,args[1])//重新的把a的,b的值取一遍。
})
}
,text(node,expr,vm){ //expr == {{a}} {{b}}
let fn = this.updater['textUpdater'];
//.+? 当前 n个 又尽量少取
let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
expr == {{a}} {{b}}
//不管b 变了,还是a变了,都
//也就是添加监控,给a,b添加监控(观察者)
new Watcher(vm,args[1],(newValue)=>{
//不能直接用newValue 替换旧值
//因为 可能是a的新值,可能是b的新值
//取完以后,作为总共的值node赋值 全的哦
fn(node,this.getContentValue(vm,expr))//返回一个全的 然后进行调用fn更新
})
return this.getValue(vm,args[1])
})
fn(node,content)
},updater:{
htmlUpdater(node,value){//xss攻击
node.innerHTML = value;
},
modelUpdate(node,value){
node.value = value
},
//处理文本节点
textUpdater(node,value){
node.textContent = value
}
}
}
Dep.js
//订阅:存放观察者
class Dep{
//如果数据一变动,就通知观察者进行依次更新
constructor(){
this.subs = []//存放所有的watcher
}
//订阅 添加观察者
addSub(watcher){
this.subs.push(watcher)
}
//如果一会数据变动了。就调用 Watcher的某个方法
//发布
notify(){
//每个观察者进行修改
//关联订阅和观察者===========================================》
this.subs.forEach(watcher=>watcher.update())
}
}
MVVM.js
class Vue{
//构造器构造Vue实例
constructor(options){
/*//console.log(options)*/
this.$el = options.el;
this.$data = options.data;
let computed = options.computed
let methods = options.methods
//判断绑定的元素是否存在----编译模板
if(this.$el){
//绑定的元素,和当前实例Vue 传递过去,因为 绑定的元素需要 vue实例去操作
///2 实现数据劫持
//把数据 全部转换成用Object.defineProperty定义
new Observer(this.$data)
//
//计算属性
//有依赖关系,
//{{newName}} reduce.
//vm.$data.getNeName
debugger;
for(let key in computed){
Object.defineProperty(this.$data,key,{
get:()=>{
return computed[key].call(this)
}
})
}
//一点击按钮,就会去执行哪个方法
//vm[change].call(vm,e)
//vm[chage]===========>methods[change]
for(let key in methods){
Object.defineProperty(this,key,{
get(){
return methods[key]
}
})
}
//把数据获取操作 vm上的取值 都代理到 vm.$data
this.proxyVm(this.$data);
new Compiler(this.$el,this)
}
}
proxyVm(data){
for(let key in data){ //this 里面有 person
Object.defineProperty(this,key,{
get(){
//debugger;
return data[key];//this.$data.person ==== this.person
},
//设置代理方法
set(newVal){
data[key] = newVal
}
})
}
}
}
Observer.js
//
class Observer{ //实现数据劫持
constructor(data){
this.observer(data)
}
observer(data){
if(data && typeof data == 'object'){
//如果是对象 才进行观察
for(let key in data){
//对象 key 值
this.defineReactive(data,key,data[key])
}
}
}
defineReactive(obj,key,value){
this.observer(value)//递归调用。。。。。。。。//person:[watcher,watcher] b:[watcher]
//改了person 就调用watcher
//给obj的key方法进行 重新设置
let dep = new Dep()//给没一个ie属性 都加上一个具有发布定语的功能1
//Object.defineProperty(对象,对象,对象)
Object.defineProperty(obj,key,{
get(){
//创建watcher时,会取到对应的target内容
//取的时候 存放这个watcher
Dep.target && dep.addSub(Dep.target)
return value;
},set:
(newVal)=>{ //{{persson.name}} = {}
debugger;
if(value != newVal){
this.observer(newVal) //所以要监控/防止 person ={a:1}被后台改了
value = newVal;
//设置的时候通知
dep.notify()
}
}
})
}
}
Watcher.js
//需要给页面上的 input {{school.name}} 加上观察者
//在哪里加呢?---》找到对应监控的文本哪里
//输入框一输入,数据一变动,就得添加观察者。
//CompileUtil中的model方法进行
//实现。数据一变动就 进行编译。
//模式: 观察者 发布订阅,被观察者
//需要将被观察者 放到观察者里面去
//第二课
//文本处理的时候加观察者,元素处理的时候也有加观察者
//一new Watcher 的时候,就会去 执行get方法
//let value = CompileUtil.getValue(this.vm,this.expr);//
class Watcher{
/*
vm.$watch(vm,'school.name',(newVal)=>{
console.log('school.name 的数据一变动就执行这个回调方法')
})
* */
constructor(vm,expr,callBack){
this.vm = vm
this.expr = expr
this.callBack = callBack
//默认先存放老值 -==》有变动才 执行回调方法
this.oldValue = this.get()
}
get(){
//vm.$data.person,vm.$data.person.name
//一获取值就会去调用Observer类的 get方法(已经被绑定了get set方法)
debugger;
Dep.target = this;// 先把自己放在this
//取值,把这个观察者 和数据关联起来
let value = CompileUtil.getValue(this.vm,this.expr);//
Dep.target = null //不取消的话,任何值取值,都会添加watcher
return value;
}
update(){//数据变化后,会调用观察者的update方法
let newValue = CompileUtil.getValue(this.vm,this.expr);
if(newValue !== this.oldValue){
this.callBack(newValue);
}
}
}
/
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
动态绑定了person.Name<input type="text" v-model="person.name" />
<br />
显示person.name:{{person.name}}<br />
显示person.age:{{person.age}}<br />
<!--如果数据不变化,视图就不会刷新-->
计算属性:{{NewName}}
<ul>
<li>1</li>
<li>2</li>
</ul>
<button v-on:click="change">修改name</button>
<div v-html="message"></div>
</div>
</body>
<!--<script type="text/javascript" src="../js/vue.js" ></script>-->
<script type="text/javascript" src="js/Compiler.js"></script>
<script type="text/javascript" src="js/CompileUtil.js"></script>
<script type="text/javascript" src="js/Dep.js"></script>
<script type="text/javascript" src="js/Observer.js"></script>
<script type="text/javascript" src="js/Wather.js"></script>
<script type="text/javascript" src="js/MVVM.js"></script>
<script type="text/javascript">
const app = new Vue({
el:"#app",
data:{
person:{
name:"wp",
age:23
},
message:"<h1>welcom</h1>"
},
computed:{
NewName:function(){
return this.person.name + '架构'
}
},methods:{
change:function(){
debugger
this.person.name = 'wp123'
}
}
})
</script>
</html>
文件分布