Vue2.x / Vue3.0 源码解析
一.
1. 数据驱动(Vue最大特点)
一.MVVM(数据驱动视图):Model-View-ViewModel
数据驱动的传统方式
利用DOM操作,来更新视图
更新后的方式:利用vm=new Vue()来进行数据的初始化和视图的更新,依旧利用了DOM操作,但是是封装在vm=new Vue()中的,不需要我们自己来应用,是其内部应用的
2. 数据劫持(是实现双向数据绑定的重要原理)
把数据劫持在一个模拟Vue对象中,一旦数据发生改变,不直接更改,先被拦截
利用Object.defineProperty(目标对象,属性,描述)函数来进行数据劫持
主要靠的是这个函数中封装的两个方法get()和set()
get() {
console.log("get", data.msg);
}
set(newValue) {
console.log("set", newValue);
if (newValue === data.msg) {
return;
}
data.msg = newValue;
}
3. 多属性的数据劫持
通过封装一个数据劫持的函数,函数中包括遍历整个对象的属性值,以及对其进行的数据劫持、数据更新,以及数据驱动视图操作
<!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>Document</title>
<div id="app">
</div>
</head>
<body>
<script src="js/vue.js"></script>
<script>
const data={
name:"aa",
age:18
};
let vm={}
function handleData(data){
Object.keys(data).forEach((key)=> {
Object.defineProperty(vm,key,{
get(){
console.log('get',data[key]);
},
set(newValue){
console.log('set',newValue);
if(newValue===data[key])
{
return
}
data[key]=newValue;//数据更新
//数据驱动视图
document.querySelector("#app").textContent=document.querySelector("#app").textContent+" "+data[key];
}
})
});
}
handleData(data)
console.log(vm.name);//get
vm.name="bb";//set
console.log(vm.age);//get
vm.age=100;
</script>
</body>
</html>
4. 属性中存在对象的数据劫持
例如:
const data = {
name: "aa",
age: 18,
friend: {
gender: 0,
name1: "bb",
},
};//friend是对象
方法:定义响应式数据,对每个属性进行数据劫持,利用递归,遍历来实现数据劫持的操作
5.数据劫持的优化方案:Proxy代理对象
Vue3.0版本进一步优化了数据劫持的方案:
不用去思考内部的属性,直接代理整个对象
<!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>Document</title>
<div id="app">{{msg}}</div>
</head>
<body>
<script src="js/vue.js"></script>
<script>
const data = {
name:'aaa',
age:18,
friend:{
gender:"女",
name:'bbb'
}
};
let vm = new Proxy(data,{
get(target,key){ //target代表目标对象,不能是具体的对象
console.log('get',target[key]);
return target[key]
},
set(target,key,value){
console.log('set',value);
if(value===target[key])
{
return
}
target[key]=value;
}
})
console.log(vm.name);
vm.age=100;
</script>
</body>
</html>
二.
1. 发布订阅
利用 o n 和 on和 on和emit来分别注册事件和触发事件。
e m i t 用 来 触 发 , 而 emit用来触发,而 emit用来触发,而on用来订阅
let vm=new Vue();//都是同一个,无法拆分成三个角色
//注册事件
//参数:事件类型,事件处理函数
vm.$on('change',()=>{
console.log('event1');
})
vm.$on('change',()=>{
console.log('event2');
})
vm.$emit('change')//事件类型,事件处理
2. 发布订阅源码
思路:
发布( e m i t ) 者 调 用 事 件 中 心 ( 触 发 事 件 ) , 然 后 订 阅 ( emit)者调用事件中心(触发事件),然后订阅( emit)者调用事件中心(触发事件),然后订阅(on)者把任务注册到事件中心,进行订阅(注册事件)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rVHNMUnD-1627296233557)(Vue2.x Vue3.0 源码解析.assets/image-20210716182850218.png)]
<!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>Document</title>
<div id="app"></div>
</head>
<body>
<script src="js/vue.js"></script>
<script>
//同一事件类型,可以有多个处理函数
class Vue1{
constructor(){
this.subs=Object.create(null)//比this.subs={}要好,代表没有属性,为空
//容器就是来存储eventType和handler的关系的
//{eventType1:[fun1,fun2..],eventType2:[],}
}
$on(eventType,handler){//注册事件,订阅,将数据注册到handler
this.subs[eventType]=this.subs[eventType]||[]
this.subs[eventType].push(handler)
}
$emit(eventType){//触发事件,不是注册事件,不能像上面一样,进行初始化,而是应该进行判断
if(this.subs[eventType])
{
this.subs[eventType].forEach(fun => {
fun()//每个eventType对应的不只有一个函数,当触发这个事件时,它包括的所有函数都会同时触发
});
}
}
}
let vm=new Vue1();
//注册事件
//参数:事件类型,事件处理函数
vm.$on('change',()=>{
console.log('event1');
})
vm.$on('change',()=>{
console.log('event2');
})
vm.$emit('change')//事件类型,事件处理
</script>
</body>
</html>
3. 观察者模式
与发布订阅模式相比,发布者与订阅者(观察者)联系更加紧密,不再单独有事件中心。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X6plTuJN-1627296233559)(Vue2.x Vue3.0 源码解析.assets/image-20210716183445822.png)]
<!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>Document</title>
</head>
<body>
<script src="js/vue.js"></script>
<script>
class Publisher{
constructor(){
this.subs=[]
}
//添加订阅者
addSub(sub){
if(sub&&sub.update()){
this.subs.push(sub)
}
}
//通知订阅者
notify(){
this.subs.forEach(w=>{
w.update()
})
}
}
class Watcher{
//获取发布者的通知信息
update(){
console.log('update');
}
}
const p=new Publisher()
const w1=new Watcher()
const w2=new Watcher()
p.addSub(w1)
p.addSub(w2)
</script>
</body>
</html>
4. Vue源码手写
通过数据劫持、模板编译等方式进行Vue源码的编译
//compiler.js
// 模板相关的操作
// dom :el
// data
class Compiler{
constructor(vm){
this.el = vm.$el
this.data = vm.$data
this.compile(this.el)
}
// 编译
compile(el){
let childNodes = el.childNodes
// 遍历 和 递归
Array.from(childNodes).forEach(node=>{
// 代码分割
console.log('this');
if(this.isTextNode(node)){ // 文本处理
console.log(this);
compileText(node)
}else if(this.isElementNode(node)){ //元素处理
this.compileElement(node)
}
if(node.childNodes){
this.compile(node) // 递归
}
})
}
// 编译文本 插值类型处理{{msg}} : 比如 hello world {{msg}} => hello world vue
compileText(node){
console.log(this);
let value = node.textContent //内容
let reg = /\{\{(.+?)\}\}/ // 正则规则
if(reg.test(value)){
// 获取 : 插值表达式的变量名
let k = RegExp.$1.trim()
console.log(k);
// 替换
node.textContent = value.replace(reg,this.data[k])
//数据变化 - 通知订阅 -update - cb -compiler dom操作
new Watcher(this.data,k,(newVlaue)=>{
console.log('compiler--callback');
node.textContent = newVlaue
console.log('view update');
})
}
}
// 编译元素
compileElement(node){
// 获取属性
let attributes = node.attributes
// 遍历所有属性
Array.from(attributes).forEach(attr=>{
// v- : v-text -> text 获取vue指令
console.log(attr);
let attrName = attr.name // 属性名
// 是否是指令 v-
if(this.isDirective(attrName)){
// 获取属性名里面 v-后面部分 --指令名称
attrName = attrName.substr(2)
console.log(attrName);
// 属性值 - data (key)
let key = attr.value
// text - 映射方法 ,不同指令 不同处理方式 就是不同函数
this.update(node,attrName,key)
}
})
}
// 调用指令函数 更新dom 内容
update(node,attrname,key){
// if(attrname=='text'){
// this.textUpdate(node,attrname,key)
// }
// if(attrname=='model'){
// this.modelUpdate(node,attrname,key)
// }
let fn = this[attrname + 'Update']
fn && fn.call(this,node,attrname,key)
}
// 代码分割
textUpdate(node,attrname,key){
node.textContent = this.data[key]
//数据变化 - 通知订阅 -update - cb -compiler dom操作
new Watcher(this.data,key,(newVlaue)=>{
node.textContent = newVlaue
})
}
modelUpdate(node,attrname,key){
// 注意 value
node.value =this.data[key]
new Watcher(this.data,key,(newVlaue)=>{
node.value = newVlaue
})
node.addEventListener('input',()=>{
this.data[key] =node.value
})
}
// 节点判断相关
isTextNode(node){
return node.nodeType === 3
}
isElementNode(node){
return node.nodeType === 1
}
isAttrNode(node){
return node.nodeType === 2
}
isDirective(attrName){
return attrName.startsWith('v-')
}
// js dom
// jquery dom
// template view data 没有状态跟踪
// mvvm : view data 状态同步 数据驱动 效率极高; 虚拟DOM js {1} {2}
}
//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实例上;
this._proxyData(this.$data)
// vm.$data => 响应式
new Observer(this.$data)
// 模板编译 : 解析指定 插值表达式..
new Compiler(this)
}
// data => 数据劫持 =>注入到Vue实例上;
_proxyData(data){
// 遍历key
Object.keys(data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(nValue){
if( data[key]===nValue){
return
}
data[key] = nValue
}
})
})
}
}
//Observer.js
class Observer{
constructor(data){
this.walk(data)
}
// 核心遍历
walk(data){
if(!data || typeof data !=='object'){
return
}
Object.keys(data).forEach(key=>{
this.defineReactive(data,key,data[key])
})
}
// 定义响应式数据
defineReactive(data,key,value){
let publisher = new Publisher()
let that = this
// console.log(data[key]);
// Maximum call stack size exceeded at Object
// this.walk(data[key])
this.walk(value)
Object.defineProperty(data,key,{
get(){
// 收集依赖 添加观察者
Publisher.target&&publisher.addSub(Publisher.target)
return value
},
set(nValue){
if( value===nValue){
return
}
// this 指向data
// console.log('this',this);
value = nValue
// 属性赋值是一个对象
that.walk(nValue)
console.log('set--notify()');
// 通知依赖
// 发送通知:数据变化 => 观察者 update() =>template dom({{ss}}=>value) =>views
publisher.notify()
}
})
}
}
//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>Document</title>
</head>
<body>
<div id="app">
hello world {{msg}}
<h2 >
sss{{msg}}
<h3>hello {{msg}}</h3>
</h2>
<h2 v-text="msg"></h2>
<input type="text" v-model='msg'>
</div>
<script src="./js/publisher.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compiler.js"></script>
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el:'#app',
data:{
msg:'hello vue1',
friend:{
name:'zhangsan',
age:28
}
}
})
</script>
</body>
</html>
//publisher.js
class Publisher{
constructor(){
this.subs = []
}
// 添加订阅者
addSub(sub){
if(sub && sub.update){
this.subs.push(sub)
}
}
// 通知订阅者
notify(){
console.log('publisher---notify()');
this.subs.forEach(w=>{
w.update() // 约定
})
}
}
// class Watcher{
// // 获取发布者的通知信息
// update(){
// console.log('update');
// }
// }
//watcher.js
class Watcher{
constructor(data,key,cb){
this.data = data
this.key = key
this.cb = cb
Publisher.target = this
this.oldValue = data[key]
}
// 获取发布者的通知信息
update(){
console.log('watcher --- update');
let newValue=this.data[this.key]
if(this.oldValue===newValue){
return
}
// 调用模板更新DOM
this.cb(newValue)
}
}