本文来源于我的github转载时注明出处

0.剧透

vue的实现,分为M-V,V-M,M-V三个阶段,第一个阶段主要利用fragement文档片段来节点劫持,使得M和V层关联起来。第二阶段,利用defineProperty使得V层的变化能让M层检测到并更新M层。第三阶段,利用了发布-订阅模式,让M层的变化实时反映到V层中,实现了手写的v-model

1.场景

首先,抛出一个问题,在一个ul下面创建100个li,并且编号
于是,就有

 var ul = document.getElementByTarName("ul");
for (var i = 0; i < 100; i++) {
var li = document.createElement('li');
   li.innerHTML = i+1;
   ul.appendChild(li)
 }

看起来操作是很容易的,但是每一次插入都会引起重新渲染,会重新重绘页面,因此会影响性能的
于是又有另一种方法,弄一个中转站,最后一次性放进去

var ul = document.getElementByTarName("ul");<br>
var inHtml = '';
for (var i = 0; i <100; i++) {
  inHtml +="\<li\>"+(i+1)+"\</li\>";
}
ul.innerHTML = inHtml;

然而这种方法不灵活,如果面对多变的dom结构,就难以操作

2.documentFragment

于是就有一种叫做文档片段的东西documentFragment,是没有父节点的最小文档对象,常用于存储html和xml文档,有Node的所有属性和方法,完全可以操作Node那样操作。
DocumentFragment文档片段是存在于内存中的,没有在DOM中,所以将子元素插入到文档片段中不会引起页面回流,因此使用DocumentFragment可以起到性能优化作用。
上面的问题就可以进一步优化。

var ul = document.getElementByTarName("ul");
     var frag = document.createDocumentFragment();
     var ihtml = '';
     for (var i = 0; i < 100; i++) {
       var li = document.createElement('li');
       li.innerHTML = "index: " + i;
       frag.appendChild(li);
     }
     ul.appendChild(frag);

3.节点劫持

既然有这样的一个中转站,那么他还可以做更多的事情。在开发中,随着代码量增加,越来越需要讲究性能,那么如果遇到需要操作很多节点的时候,直接创建节点的时候,页面就不断重排重绘,GPU负担越来越大。这时候,需要一个中转站,将需要用到的节点劫持,让他不在dom中
html部分:

 <div id="app">
你看见我了
<p>hi</p>
</div>

js部分:

function myFragment(node){
        var frag = document.createDocumentFragment()
        var child
        while(child = node.firstChild){//有子节点的时候,就给child赋值
            frag.appendChild(child)//追加到frag,子节点少一个
        }
        return frag
    }
    var DOM = myFragment(document.getElementById('app'))
    console.log(DOM)
    console.log('这是innerHTML:'+document.getElementById('app').innerHTML)

控制台
1

先创建一个文档片段,再将节点的第一个子节点添加到文档片段里面,再第二个……直到没有,跳出循环,此时innerhtml没有内容,都在文档片段里面了。这就是节点劫持,无论怎么改样式,整个div没有内容高度也是0。

4.看看劫持的是什么(扫描)

在上面的基础上,我们可以看一下每一个标签、每一个属性的怎样的

html:

 <div id="app">
        <input type="text" name="hi" size="1" v-model="text"\>
    </div>

在frag.appendChild(child)这句前面加上一段代码来看一下里面的节点

js:

 function myFragment(node){
        var frag = document.createDocumentFragment()
        var child
        while(child = node.firstChild){
            if(child.nodeType === 1){//如果是元素节点
            var attr = child.attributes //将元素节点所有的属性集合存放在attr
            console.log(child.attributes)
        }
            frag.appendChild(child)//将子节点追加到文档片段。非常重要,没有这句就死循环
        }
        return frag
        }
        myFragment(document.getElementById('app'))

1

手滑,不小心写多了一个v-model=”text”,不过还是被显示到了
v-model?这不就是vue的一个指令吗
既然能拿到他,那么我们现在开始手写一个迷你版vue试试看

5.迷你版vue准备工作

一贯使用的IIFE
对于全局环境,存在exports对象的话,说明引入环境是node或者其他commonjs环境。如果是amd标准,如requirejs,就用define(factory)引入逻辑代码

(function(global,factory){
    typeof exports === 'object'&& typeof module !== 'undefined'?module.exports = factory():
    typeof define === 'function' && define.amd?define(factory) :
    (global.Vue = factory())
})(this,function(){
//主体在这里
})

这段国际常规的hello word代码放在最后

var app = new Vue({
        el:"app",
        data:{
            text:"hello word",
            message:{name:'pp'}
        }
    })

6.M-V绑定

data中的值,反映到input中,也就是M->V层的过程
html:

<div id="app">
        <input v-model="text" type="text" name="n" size="10" \>
        {{text}}
    </div>

6.1定义Vue构造函数

传入的参数就是new Vue里面的对象,获得el、data,再劫持id为app的元素里面的节点,并进行操作

var Vue = function(opts){
            var id = opts.el||body
            this.data = opts.data||{}
            var DOM = myFragment(document.getElementById(id),this)
            document.getElementById(id).appendChild(DOM)//劫持到节点,添加到app上
        }

6.2myFragment方法的完善

上面已经讲到怎么劫持节点,并console看到了节点的内容
遍历attr,如果发现v-model这个属性,就给他赋值,此时输入框内容就是hello word

for(var i = 0;i<attr.length;i++){
                if(attr[i].nodeName == 'v-model'){
                    var name = attr[i].nodeValue
                    console.log(name) //text
                                        node.value = vm.data[name]//输入框内容:hello word
                }
            }

6.3替换mustache的内容

已经搞定了输入框,接下来就是双大括号了{{ }},继续在扫描的方法中添加另一个分支:当扫描到文本节点,就使用正则匹配双大括号并进行替换

if(node.nodeType === 3){//匹配文本节点
            if(/\{\{(.*)\}\}/.test(node.nodeValue)){
                var name = RegExp.$1//获得文本内容
                console.log(name)
                name = name.trim()
                node.nodeValue = vm.data[name]//替换双大括号的内容
            }
        }

1
现在,文本框和双大括号值都是hello world 了
注意:vm.data[name]可以理解为初步绑定,他就是data里面的text的内容,接下来肯定不是绑死他的

6.4数据监听

定义一个observer函数,彻底地监听每一个数据,而且需要无视对象中的对象。先检测obj是不是对象类型,如果不是就跳出(此时已经是对象多层嵌套的最里面那层的key),如果是对象,就调用calation方法递归。

function observer(obj,vm){
            if(typeof obj!=='object'){return}
                Object.keys(obj).forEach(function(key){
                    console.log(key)//text,message,name
                    calation(vm,obj,key,obj[key])
                })
        }
function calation(vm,obj,key,value){
            observer(value,vm)
        }

综上,在IIFE主体里面添加下面代码,这部分是M->V的过程

var Vue = function(opts){
            var id = opts.el||body
            this.data = opts.data||{}
            var DOM = myFragment(document.getElementById(id),this)
            document.getElementById(id).appendChild(DOM)
        }
        function myFragment(node,vm){
        var frag = document.createDocumentFragment()
        var child
        while(child = node.firstChild){
            comp(child,vm)
            frag.appendChild(child)
        }
        return frag
    }
    function comp(node,vm){
        if(node.nodeType === 1){
            var attr = node.attributes
            for(var i = 0;i<attr.length;i++){
                if(attr[i].nodeName == 'v-model'){
                    var name = attr[i].nodeValue
                    console.log(name)
                    node.value = vm.data[name]
                }
            }
        }
        if(node.nodeType === 3){
            if(/\{\{(.*)\}\}/.test(node.nodeValue)){
                var name = RegExp.$1
                console.log(name)
                name = name.trim()
                node.nodeValue = vm.data[name]
            }
        }
    }
        function observer(obj,vm){
            if(typeof obj!=='object'){return}
                Object.keys(obj).forEach(function(key){
                    console.log(key)
                    calation(vm,obj,key,obj[key])
                })  
        }
        function calation(vm,obj,key,value){
            observer(value,vm)
        }
return Vue

第一次M-V绑定,可以说是初始化,就是让input和Vue的实例对象里面传入的参数中的data联系起来,也就是‘’搭建起沟通的桥梁‘’

7.V-M绑定

用户输入改变input的值(V层)时,data中(M层)也改变对应的值

7.1关于defineProperty

终于到了江湖中流传的defineProperty了,这个api究竟是怎么用的,先举个小栗子

var obj = {name:'pp'}
console.log(obj.name)//pp
Object.defineProperty(obj,'name',{
      get:function(){
        return 1
      },
      set:function(newVal){
        console.log(newVal)
      }
    })
console.log(obj.name)//1
obj.name = 2;//2
console.log(obj.name)//1

当访问这个属性的时候,调用的是get方法,这里输出1,当试图改变属性的值的时候,调用的是set方法,console这个值,也就是这里输出2的原因。再次回头访问,还是输出1。(我这里set方法只是console而已,再回头看obj.name当然还是1)

7.2 即时反映单向数据变化的demo

html:

<input id="app" type="text" \>
        <p id="p"></p>

js:

document.getElementById('app').addEventListener('input',function(e){
        document.getElementById('p').innerHTML=e.target.value;
    })

回过头来,我们的vue也是要这样做的

7.3在带有属性v-model上添加事件监听

在comp函数里面,匹配到了v-model=‘text’ 这个属性时,取得v-model的属性的值text,Vue的实例对象vm的text属性的值,等于输入框更新的值。输入框输入什么,这个

data:{
            text:"hello word",
            message:{name:'pp'}
        }

里面的 text就是什么,不再是helloworld了(前面数据监听的时候,有做过observer的递归,所以无论多少层嵌套对象,总会能彻底取得key-value的形式)

if(attr[i].nodeName == 'v-model'){
                    var name = attr[i].nodeValue
                     node.addEventListener('input',function(e){
                    vm[name]=e.target.value;//Vue的实例对象vm的text属性的值,赋值并触发该属性的set函数
          });

接着,把输入框改变的值赋值node.value = vm[name],前面是node.value = vm.data[name]的初步尝试,让input和data关联起来,现在需要改

同理,文本节点那里也要改(为最后一步做铺垫,当然现在还是没有效果)
通过正则获得双大括号里面的值(text),定义一个name=’text’ ,从而能改变双大括号的值
node.nodeValue=vm[name];

7.4监听属性

再定义一个监听器defineReactive,在observer里面执行,用到了Object.defineProperty

function defineReactive(obj,key,val){
            Object.defineProperty(obj,key,{
              get:function(){
                return val
              },
              set:function(newVal){
                if(newVal===val)return ;
                val=newVal;//数据在改变
                console.log(val)
              }
            })
          }

递归完成后就开始监听属性
>function observer(obj,vm){
            if(typeof obj!=='object'){return}
                Object.keys(obj).forEach(function(key){
                    console.log(key)
                    calation(vm,obj,key,obj[key])
                    defineReactive(vm,key,obj[key])
                }) 
        }

现在,输入框写了什么,就console了什么

8.M-V再次绑定

这次是,当用户主动改变M层数据,V层也跟着改变,第一次是默认的,只是让他们建立起关联。(其实这就是鸡生蛋,蛋生鸡的过程,总得有一个开头吧,为什么不VMMV而是MVVM,也可以想到,难道一个软件需要用户设置初始值?那么真的需要用户设置初始值呢?那就第一次MV给他设置默认值为空,前面也有处理)

8.1初探发布-订阅模式(观察者模式)

它是一种一对多的关系,让多个订阅者(也可以叫观察者)者对象同时监听某一个主题对象,当一个主题对象发生改变时,发布者将会发布变化的通知,所有依赖于它的对象都(订阅者)将得到通知。多个订阅者对象监视主题对象,当发生变化,就由发布者通知订阅者

//定义2个订阅者
var subscriber1 = {update:function(){console.log(1)}}
var subscriber2 = {update:function(){console.log(2)}}
var pub = {//定义发布者
    publish:function(){
        dep.notify()//主题对象的实例调用发布通知
    }
}
function Dep(){//主题对象构造函数
    this.subs=[ subscriber1, subscriber2]
}
Dep.prototype.notify = function(){//主题对象的原型上定义通知函数
    this.subs.forEach(function(sub){//通知每一个订阅者并执行相应的方法
        sub.update()
    })
}
var dep = new Dep()//主题对象实例化
pub.publish()//发布者发布信息

最后控制台打印结果就是1,2

8.2监听器defineReactive中绑定主题对象与订阅者

data每一个属性被监听的时候添加一个主题对象,当data发生改变将触发Object.defineProperty里面的set方法,去通知订阅者们

function Dep(){
    this.subs=[];//订阅者集合
  }
  Dep.prototype={
    addSub:function(sub){//主题对象的原型上添加订阅者的方法
      this.subs.push(sub);
    },
    notify:function(){ //发布信息
      this.subs.forEach(function(sub){
        sub.update();//订阅者的方法
      })
    }
  }

在Object.defineProperty方法前面实例化Dep:var dep=new Dep();
那么sub.update()的订阅者方法呢,接下来将会解释

8.3订阅者的定义

观察主题对象(有v-model属性的input)变化,将变化展示到视图层(双大括号里面)

function Watcher(vm,node,name){
    Dep.target=this;//Dep的静态属性target指向当前订阅者的实例
    this.name=name;
    this.node=node;
    this.vm=vm;
    this.update(); //先初始化视图
    Dep.target=null;
  }
  Watcher.prototype={
    get:function(){
      this.value=this.vm[this.name]//得到实例对象的属性的值
    },
update:function(){
      this.get();
      this.node.nodeValue=this.value;
    }
  }

再回到获得文本节点的时候(if(node.nodeType === 3))
在内部最后一句加上 new Watcher(vm,node,name); 实例化订阅者

8.4 监听器defineReactive的get与set

在comp方法中,通过初始化value值,触发set函数,在set函数中为主题对象添加订阅者。
在defineProperty的get方法中当某个订阅者存在,就添加订阅者

get:function(){
                if(Dep.target){dep.addSub(Dep.target)}
                return val
              },

set方法改变了数据后,主题对象的实例发布通知

set:function(newVal){
                if(newVal===val){return ;}
                val=newVal;
                console.log(val)
                 dep.notify();
              }

9.大功告成

终于全部搞定了,上完整代码

html:

><div id="app">
        <input v-model="text" type="text" name="n" size="10" \>
        {{text}}
 </div>

js:

javascript
(function(global,factory){
typeof exports === 'object'&& typeof module !== 'undefined'?module.exports = factory():
typeof define === 'function' && define.amd?define(factory) :
(global.Vue = factory())
})(this,function(){
var Vue = function(opts){
var id = opts.el||body
this.data = opts.data||{}
data = this.data
observer(data,this)
var DOM = myFragment(document.getElementById(id),this)
document.getElementById(id).appendChild(DOM)
}
function myFragment(node,vm){
var frag = document.createDocumentFragment()
var child
while(child = node.firstChild){
comp(child,vm)
frag.appendChild(child)
}
return frag
}
function comp(node,vm){
if(node.nodeType === 1){
var attr = node.attributes
for(var i = 0;i<attr.length;i++){
if(attr[i].nodeName == 'v-model'){
var name = attr[i].nodeValue
console.log(name)
node.addEventListener('input',function(e){
vm[name]=e.target.value;
//console.log('vm[name]'+vm[name])
//console.log('vm.data[name]'+vm.data[name])
});
node.value = vm[name]
}
}
}
if(node.nodeType === 3){
if(/\{\{(.*)\}\}/.test(node.nodeValue)){
var name = RegExp.$1
console.log(name)
name = name.trim()
node.nodeValue=vm[name];
new Watcher(vm,node,name);
}
}
}
function observer(obj,vm){
if(typeof obj!=='object'){return}
Object.keys(obj).forEach(function(key){
console.log(key)
calation(vm,obj,key,obj[key])
defineReactive(vm,key,obj[key])
})
}
function calation(vm,obj,key,value){
observer(value,vm)
}
function defineReactive(obj,key,val){
var dep=new Dep();
Object.defineProperty(obj,key,{
get:function(){
if(Dep.target){dep.addSub(Dep.target)}
return val
},
set:function(newVal){
if(newVal===val)return ;
val=newVal;
// console.log(val)
dep.notify();
}
})
}
function Dep(){
this.subs=[];
}
Dep.prototype={
addSub:function(sub){
this.subs.push(sub);
},
notify:function(){
this.subs.forEach(function(sub){
sub.update();
})
}
}
function Watcher(vm,node,name){
this.vm=vm;
this.node=node;
this.name=name;
Dep.target=this;
this.update();
Dep.target=null;
}
Watcher.prototype={
update:function(){
this.get();
this.node.nodeValue=this.value;
},
get:function(){
this.value=this.vm[this.name]
}
}
return Vue
})
//引入了vue,开始常规操作
var app = new Vue({
el:"app",
data:{
text:"hello word",
message:{name:'pp'}
}
})

Logo

前往低代码交流专区

更多推荐