从documentfragement到实现手写vue

本文是lhyt本人原创,希望用通俗易懂的方法来理解一些细节和难点。转载时请注明出处。文章最早出现于本人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");

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)

控制台

先创建一个文档片段,再将节点的第一个子节点添加到文档片段里面,再第二个......直到没有,跳出循环,此时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'))

手滑,不小心写多了一个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]//替换双大括号的内容

}

}

现在,文本框和双大括号值都是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'}

}

})


原文来源于:lhyt的github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,727评论 2 17
  • 单例模式 适用场景:可能会在场景中使用到对象,但只有一个实例,加载时并不主动创建,需要时才创建 最常见的单例模式,...
    Obeing阅读 2,058评论 1 10
  • 深入响应式 追踪变化: 把普通js对象传给Vue实例的data选项,Vue将使用Object.defineProp...
    冥冥2017阅读 4,854评论 6 16
  • 我实在无法把目光从他脸上移开,那样一张小丑的脸。 它对非日常性、犯罪、畸形与病变的暗示令人兴奋。我想知道,一个人可...
    Pinocchio阅读 2,466评论 3 20
  • 话说某一天去游族面试,被问到知不知道HanderThread,当时就懵逼了,梗了半天回答不出来,没有了解过...结...
    boboyuwu阅读 272评论 0 0