从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 +="

  • "+(i+1)+"
  • ";

    }

    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部分:

    你看见我了

    hi

    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:

    在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'))

    从documentfragement到实现手写vue_第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:

    {{text}}

    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

        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:

    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}}

    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

    你可能感兴趣的:(从documentfragement到实现手写vue)