本文主要根据Vue数据响应原理和特点实现一个简单的 mvvm 框架。和真正的源码相比简化了许多,没有虚拟dom,diff算法等核心内容,主要实现了数据劫持,双向绑定功能。
项目创建
<!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">
<p>{{counter}}</p>
<p v-html="desc"></p>
</div>
<!--不再引用cdn 上的vue-->
<!-- <script src="../node_modules/vue/dist/vue.js"></script> -->
<script src="myvue.js"></script>
<script>
const app = new MyVue({
el: '#app',
data: {
counter: 1,
desc:'<p>快来给我<span style="color:red">点赞</span>吧</p>'
}
})
setInterval(() => {
app.counter++
}, 1000);
</script>
</body>
</html>
我们的目标就是将数据MyVue中的数据渲染视图层view上,并且当用户交互时,修改model中数据,同时改变依赖该数据的view层节点,更新显示新的数据。
需求分析
我们的思路大致如下图所示
- 建立 observer类用于监听数据变化,并且负责监听数据变化
- observer在遍历属性劫持数据的过程中,创建 dep实例,用于添加订阅者wathcer(依赖收集),以及今后通知变化
- 解析模板,创建watch实例用于订阅数据变化并且绑定更新函数,同时初始化视图
数据劫持和Observer
function observe(obj){
if(typeof obj !=='object' || obj===null ){
return obj
}
new Observer(obj)
}
class Observer{
constructor(obj){
//是否是数组判断
if(Array.isArray(obj)){
//todo 数组变化
}else{
this.walk(obj)
}
}
walk(obj){
Object.keys(obj).forEach(key=>{
defineReactive(obj,key,obj[key])
})
}
}
function defineReactive(obj,key,value){
//递归遍历
//劫持数据
observe(value);
Object.defineProperty(obj,key,{
get(){
console.log(`get ${key}=${value}`)
return value
},
set(val){
//需要这样写
if (val !== value) {
//如果传入的是对象,需要继续观测
observe(val)
console.log(`set ${key}=>${val}`);
value=val
}
}
})
}
//测试
const data={
counter: 1,
desc:'<p>YOYO很棒</p>',
obj:{
foo:'foo1',
bar:'bar1'
}
}
observe(data)
setTimeout(()=>{
data.counter=2; //set counter=>2
data.desc; //get desc=<p>YOYO很棒</p>
data.obj.foo="foo2" //get obj=[object Object]因为先读的obj.obj set foo=>foo2
},1000)
初始化编译
//myVue类
class MyVue{
constructor(options) {
//保存选项
this.$options = options;
this.$data = options && options.data;
//观测数据
observe(this.$data)
//代理data 到 vm 实例上 this.$data.counter => this.counter
proxy(this)
//遍历dom树,找到动态的表达式或者指令等,依赖收集
new Compile(this.$el,this);
}
}
//代理data到vm实例上
function proxy(vm){
Object.keys(vm.$data).forEach(key=>{
Object.defineProperty(vm,key,{
// this.counter => this.$data.counter
get(){
console.log(`proxy, get ${key} ${vm.$data[key]}`)
return vm.$data[key]
},
set(val){
if(val!==vm.$data[key]){
vm.$data[key]=val
}
}
})
})
}
// 遍历dom树,找到动态的表达式或者指令等
class Compile {
constructor(el, vm) {
this.$el = document.querySelector(el);
this.$vm = vm;
if (this.$el) {
this.compile(this.$el);
}
}
//递归遍历所有节点
compile(el) {
const childNodes = el.childNodes;
childNodes.forEach((node) => {
//如果是元素节点
if (node.nodeType == 1) {
//取出所有子节点进行遍历
this.compileElement(node);
//元素节点下有子节点
if (node.hasChildNodes()) {
//递归遍历
this.compile(node);
} // 形如{{xx}}
} else if (this.isInter(node)) {
//处理动态表达式
this.compileText(node);
}
});
}
//所有的初始化及更新操作通过update转发
//exp--counter dir--text
update(node, exp, dir) {
//初始化
const fn = this[dir + "Updater"];
fn && fn(node,this.$vm[exp])
}
//处理 v-text
textUpdater(node, value) {
node.textContent = value;
}
//处理 v-html
htmlUpdater(node,value){
node.innerHTML=value
}
//处理元素节点
compileElement(node) {
//处理指令内容
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach((attr) => {
const attrName = attr.name; // v-text
const exp = attr.value; // xxx
console.log(`attrName`, attrName);//v-text
console.log(`exp`, exp);//desc
let dir
if(attrName){
dir=attrName.slice(2)
}
this.update(node,exp,dir)
})
}
compileText(node) {
//RegExp.$1 -- counter
this.update(node, RegExp.$1, "text");
}
// 形如{{xx}}
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
初始化编译成功
依赖收集
视图中会用到data中的某个key,这就称为依赖. 同一个key可能出现多次. 每次都需要收集出来用一个watcher维护,此过程称为依赖收集. 多个watcher需要统一一个Dep管理,更新时需要这个Dep统一通知
实现思路
- defineReactive时为每个key创建一个Dep实例
- 初始化视图时读取某个key,比如name1,创建watcher1
- 利用name1读取时触发getter方法,将watcher1 添加到对应dep实例中
- 当name1更新触发setter方法,通过对应dep实例通知其管理的所有watcher
class Compile{
//...
update(node, exp, dir) {
//内层是 updater
//初始化
const fn = this[dir + "Updater"];
console.log(`this.$vm[exp]`,this.$vm[exp])
fn && fn(node,this.$vm[exp])
//wacher绑定更新函数,用于执行今后数据的变更
new Wathcer(this.$vm,exp,val=>{
fn(node,val)
})
}
//...
}
class Wathcer{
constructor(vm,exp,fn){
this.$vm=vm;
this.key=exp;
this.updaterFn=fn;
//这里是依赖收集的关键
Dep.target=this;
//要触发 defineProperty的get
vm[this.key]
Dep.target=null
}
//未来执行更新时用
update(){
this.updaterFn.call(this.$vm,this.$vm[this.key])
}
}
class Dep{
constructor(){
//用于放置 watcher
this.deps=[];
}
//保存watcher
addDep(watcher){
this.deps.push(watcher)
}
//通知变更
notify(){
this.deps.forEach(watcher=>{
watcher.update()
})
}
}
function defineReactive(obj,key,value){
//递归遍历
//劫持数据
const dep=new Dep() //每一个key对应会有一个dep
observe(value);
Object.defineProperty(obj,key,{
get(){
console.log(`get ${key}=${value}`)
//每一个绑定会生成watcher,在这里被收集
if(Dep.target){
dep.addDep(Dep.target)
}
return value
},
set(val){
//需要这样写
if (val !== value) {
console.log(`set ${key}=>${val}`);
observe(val)
value=val
//变更时触发变更
dep.notify()
}
}
})
}
完整myVue代码
function observe(obj){
if(typeof obj !=='object' || obj===null ){
return obj
}
new Observer(obj)
}
function defineReactive(obj,key,value){
//递归遍历
//劫持数据
const dep=new Dep()
observe(value);
Object.defineProperty(obj,key,{
get(){
console.log(`get ${key}=${value}`)
if(Dep.target){
dep.addDep(Dep.target)
}
return value
},
set(val){
//需要这样写
if (val !== value) {
console.log(`set ${key}=>${val}`);
observe(val)
value=val
dep.notify()
}
}
})
}
class Observer{
constructor(obj){
//是否是数组判断
if(Array.isArray(obj)){
//todo 数组变化
}else{
this.walk(obj)
}
}
walk(obj){
Object.keys(obj).forEach(key=>{
defineReactive(obj,key,obj[key])
})
}
}
class MyVue{
constructor(options) {
//保存选项
this.$options = options;
this.$el = options && options.el;
this.$data = options && options.data;
//观测数据
observe(this.$data);
//代理 data 到 vm上
proxy(this)
this.counter
new Compile(this.$el, this);
}
}
function proxy(vm){
Object.keys(vm.$data).forEach(key=>{
Object.defineProperty(vm,key,{
// this.counter => this.$data.counter
get(){
return vm.$data[key]
},
set(val){
if(val!==vm.$data[key]){
vm.$data[key]=val
}
}
})
})
}
// 遍历dom树,找到动态的表达式或者指令等
class Compile {
constructor(el, vm) {
this.$el = document.querySelector(el);
this.$vm = vm;
if (this.$el) {
this.compile(this.$el);
}
}
//递归遍历所有节点
compile(el) {
const childNodes = el.childNodes;
childNodes.forEach((node) => {
if (node.nodeType == 1) {
//取出所有子节点进行遍历
this.compileElement(node);
//元素节点下有子节点
if (node.hasChildNodes()) {
this.compile(node);
} // 形如{{xx}}
} else if (this.isInter(node)) {
this.compileText(node);
}
});
}
//所有的初始化更新操作通过update转发
//exp--counter dir--text
update(node, exp, dir) {
//内层是 updater
//初始化
const fn = this[dir + "Updater"];
console.log(`this.$vm[exp]`,this.$vm[exp])
fn && fn(node,this.$vm[exp])
//新增
new Wathcer(this.$vm,exp,val=>{
fn(node,val)
})
}
textUpdater(node, value) {
node.textContent = value;
}
htmlUpdater(node,value){
console.log(`value`,value)
node.innerHTML=value
}
//更新函数
compileElement(node) {
//处理指令内容
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach((attr) => {
const attrName = attr.name; // v-text
const exp = attr.value; // xxx
console.log(`attrName`, attrName);//v-text
console.log(`exp`, exp);//desc
let dir
if(attrName && attrName.indexOf("v-")==0){
dir=attrName.slice(2)
}
this.update(node,exp,dir)
})
}
compileText(node) {
//RegExp.$1 -- counter
this.update(node, RegExp.$1, "text");
}
isInter(node) {
// 形如{{xx}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
//新增
class Wathcer{
constructor(vm,exp,fn){
this.$vm=vm;
this.key=exp;
this.updaterFn=fn;
Dep.target=this;
//要出发
vm[this.key]
Dep.target=null
}
//未来执行更新时用,不需要使用新的val
update(){
this.updaterFn.call(this.$vm,this.$vm[this.key])
}
}
class Dep{
constructor(){
//用于放置 watcher
this.deps=[];
}
addDep(watcher){
this.deps.push(watcher)
}
notify(){
this.deps.forEach(watcher=>{
watcher.update()
})
}
}