Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从documentfragement到实现手写vue #2

Open
lhyt opened this issue Jan 5, 2018 · 2 comments
Open

从documentfragement到实现手写vue #2

lhyt opened this issue Jan 5, 2018 · 2 comments
Assignees

Comments

@lhyt
Copy link
Owner

lhyt commented Jan 5, 2018

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:

(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'}
        }
    })
@killopept
Copy link

厉害了老铁,💖💖💖💖💖,还不是看很懂

@14glwu
Copy link

14glwu commented Aug 25, 2018

厉害了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants