MVVM源码解析

本文深入剖析了简易Vue.js框架的实现原理,包括数据代理、数据响应化、模板解析等核心概念,展示了如何通过数据劫持和发布订阅模式实现MVVM架构。

简易vue.js框架(MVVM)源码解析

学习准备

  • [].slice.call()
  • node.nodeType 节点类型
  • Objcet.defineproperty
  • Object.hasOwnProperty
  • DocumentFragment DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。
    const ul = doucment.getElementById('xxx')
    // 创建一个fragment
    const fragment = doucment.createDocumentFragment()
    // 取出ul中的所有子节点保存到fragment
    let child 
    while(child=ul.firstChild){
    	fragment.appendChild(child)  //先将child从ul中移除,添加为fragment子节点
    }
    // 更新
    Array.prototype.slice.call(fragment.childNodes).forEach((node)=>{
    if(node.nodeType===1){
    	node.textContent = 'xxxx'
    }
    // 将fragment插入到dom中
    ul.appendChild(fragment)
    })
    
    

数据代理

  1. 数据代理:通过一个对象代理另一个 对象中属性的操作(读/写)
  2. vue数据代理:通过vm对象代理vm.data对象中所有属性的操作
  3. 好处:方便操作data中的所有数据
  4. 基本实现流程
    a. 通过object.defineproperty给vm添加与data对象的属性对应的属性描述符
    b. 所有添加的属性都包含getter/setter
    c. getter/setter内部去操作data中对应的属性数据
    function Vue(options){
            this.$options = options
            var data = this._data = this.$options.data
            var me = this;
            observe(this._data)
            function observe(value){
                // 判断kvue中的data是否存在,且为object
                if(!value || typeof value !=='object'){
                    return
                }
                // 遍历该对象
                Object.keys(value).forEach(key=>{
                    defineReactive(value,key,value[key])
                    // 代理data中的属性到实例上
                    proxyData(key)
                })
            }
            // 数据响应化
            function defineReactive(obj,key,val){
                if(typeof obj ==='object'){
                    observe(val);  //递归 对象嵌套数据的劫持
                }
                Object.defineProperty(obj,key,{
                    get(){
                        return val
                    },
                    set(newVal){
                        if(newVal===val){
                            return 
                        } else {
                            val = newVal;
                        }
                    }
                })
            }
            // proxyData
            function proxyData(key){
                Object.defineProperty(me,key,{
                    get(){
                        return me._data[key]
                    },
                    set(newVal){
                        me._data[key] = newVal
                    }
                })
            }
        }
        const vm = new Vue({
            el:'#app',
            data:{
                name:"name值",
                age:18
            }
        })
        console.log(vm._data.name,vm.age)
    

模板解析

  1. 创建一个fragment,把所有的原生节点拷贝到fragment中
  2. 然后使用init方法编译
  3. 把编译过后的fragment添加到el中
    function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
        }
    }
    Compile.prototype = {
    	init: function() { this.compileElement(this.$fragment); },
        node2Fragment: function(el) {
            var fragment = document.createDocumentFragment(), child;
            // 将原生节点拷贝到fragment
            while (child = el.firstChild) {
                fragment.appendChild(child);
            }
            return fragment;
        }
    };
    Compile.prototype = {
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;	// 表达式文本
            // 按元素节点方式编译
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },
    
    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = attr.name;	// v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                // 从属性表达式上拿到指令类型
                var dir = attrName.substring(2);	// text
                if (me.isEventDirective(dir)) {
                	// 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
    };
    var compileUtil = {
        text: function(node, vm, exp) {
            this.bind(node, vm, exp, 'text');
        },
        bind: function(node, vm, exp, dir) {
            var updaterFn = updater[dir + 'Updater'];
            // 第一次初始化视图
            updaterFn && updaterFn(node, vm[exp]);
            // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
            new Watcher(vm, exp, function(value, oldValue) {
            	// 一旦属性值有变化,会收到通知执行此更新函数,更新视图
                updaterFn && updaterFn(node, value, oldValue);
            });
        }
    };
    
    // 更新函数
    var updater = {
        textUpdater: function(node, value) {
            node.textContent = typeof value == 'undefined' ? '' : value;
        }
    };
    

下面是简易MVVM的实现

class Compiler{
    constructor(el,vm){
        // 判断el属性是不是一个元素,如果不是元素就获取它
        this.el = this.isElementNode(el)?el:document.querySelector(el);
        this.vm = vm;
        // 把当前节点中的元素获取到,放到内存中
        let fragment = this.node2fragment(this.el);
        // 把节点中的内容进行替换
        // 编译模板 用数据编译
        this.compile(fragment);
        // 把内容再塞到页面中
        this.el.appendChild(fragment);
    }
    // 判断是不是元素节点的方法
    isElementNode(node){
        return node.nodeType === 1;  // 是不是元素节点
    }
    // 把dom节点移动到内存中
    node2fragment(node){
        // 创建一个文档碎片 createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。
        let fragment = document.createDocumentFragment();
        let firstChild;
        while(firstChild = node.firstChild){
            // appendChild() 方法向节点添加最后一个子节点,也可以使用 appendChild() 方法从一个元素向另一个元素中移动元素。
            fragment.appendChild(firstChild)
        }
        // console.log(fragment);
        return fragment;
    }
    // 核心的编译方法
    compile(node){
        let childNodes = node.childNodes; // 此时的 childNodes是一个类数组
        [...childNodes].forEach((child)=>{
            if(this.isElementNode(child)){
                this.compileElement(child)
                // 如果是元素的话,需要把自己传进去再遍历子节点
                this.compile(child); // 递归遍历子节点
            } else {
                this.compileText(child)
            }
        })
    }
    // 编译元素的方法
    compileElement(node){
        let attributes = node.attributes; // 获取每一个节点的属性
        [...attributes].forEach(attr => {
            let {name,value:expr} = attr
            // 判断是不是指令
            if(this.isDirective(name)){
                let [,directive] = name.split('-') // 解构赋值,获取v-xx指令中的 xx后缀
                // 根据不同的(xx后缀)指令进行不同的处理
                CompileUtil[directive](node,expr,this.vm) // expr 就是属性值 例如type="input" v-mode="school.name" 中的'input'和'school.name'
            }

        });
    }
    // 编译文本的方法
    compileText(node){
       let content = node.textContent;
       if(/\{\{(.+?)\}\}/.test(content)){  // 满足插值表达式的文本
        CompileUtil['text'](node,content,this.vm) 
       }
    }
    // 判断是不是指令
    isDirective(attrName){
        // startsWith() 方法用于检测字符串是否以指定的子字符串开始
        return attrName.startsWith('v-')
    }
}
CompileUtil = {
    // 根据表达式取到对应的数据
    getVal(vm,expr){
        return expr.split('.').reduce((data,current)=>{
            return data[current]
        },vm.$data)
    },
    // 设置val的值
    setVal(vm,expr,value){
        // {{schoo.name}}  // 取到name的值,并完成最终赋值
          expr.split('.').reduce((data,current,index,arr)=>{
             if(index == arr.length-1){
                return data[current] = value
             }
            return data[current]
        },vm.$data)
    },
    // 处理 v-model指令的方法
    model(node,expr,vm){ // node 节点 expr属性值 vm 实例
        let fn = this.updater['modelUpdater'];
        // 给输入框添加一个watcher
        new Watcher(vm,expr,(newVal)=>{
            fn(node,newVal)
        })
        // 监听带有v-model标签的input事件
        node.addEventListener('input',(e)=>{
            let value = e.target.value;
            // 如果input事件触发的话 更行视图
            this.setVal(vm,expr,value);
        })
        let value = this.getVal(vm,expr); // 通过属性名 去vm上获取data中的值
        fn(node,value)
    },
    html(){

    },
    getContentValue(vm,expr){
        // 编辑表达式,将内容重新替换一个完整的内容,返还回去
        return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getVal(vm,args[1])
        })
    },
    text(node,expr,vm){
        // expr => {{a}} {{b}} {{c}} 
        let fn = this.updater['textUpdater'];
        // 给表达式的每一个变量都增加一个watcher
        let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
        new Watcher(vm,args[1],()=>{
             // 返回了一个全的字符串
            fn(node,this.getContentValue(vm,expr))
        })
            return this.getVal(vm,args[1])
        })
        fn(node,content)
    },
    updater:{
        modelUpdater(node,value){
            node.value = value
        },
        htmlUpdater(){
           
        },
        textUpdater(node,value){
            console.log(value);
            node.textContent = value
        }
    }
}

class Observer{  // 实现数据劫持功能
    constructor(data){
        this.observer(data);
    }
    observer(data){
        if(data && typeof data == 'object'){
            // 如果是对象
            for(let key in data){
                this.defineReactive(data, key, data[key]);
            }
        }
    }
    defineReactive(obj,key,value){
        // 递归遍历value(value)可能也是一个对象
        this.observer(value);
        let dep = new Dep() // 给每一个属性都加上一个具有发布订阅的功能
        Object.defineProperty(obj,key,{
            get(){
                // 创建watcher时 会取到对应的内容,并把watcher放在了全局上
                Dep.target && dep.addSub(Dep.target)
                return value;
            },
            set:(newVal)=>{
                // 如果是一个新值的话,那就重新劫持数据
                if(newVal != value){
                    this.observer(newVal)
                    value = newVal
                    dep.notify(); // 通知watcher,调用update
                }
                
            }
        })
    }
}

class Dep {
    constructor(){
        this.subs = []; // 这里存放所有的watcher
    }
    // 订阅
    addSub(watcher){ // 添加watcher
        this.subs.push(watcher)
    }
    // 发布
    notify(){
        this.subs.forEach(watcher => watcher.update());
    }
}
class Watcher{ // 观察者
    constructor(vm,expr,cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 默认先存一个老值
        this.oldValue = this.get()

    }
    get(){
        Dep.target = this; // 把watcher放在target上
        let value = CompileUtil.getVal(this.vm,this.expr);  // 取值 把这个观察者和数据关联起来
        Dep.target = null; 
        return value;
    }
    update(){ // 更新操作 数据变化后 会调用观察者的update方法
        let newVal = CompileUtil.getVal(this.vm,this.expr);
        if(newVal !== this.oldValue) {
            this.cb(newVal)
        }
    }
    

}
class Vue{
    constructor(options){
        this.$el = options.el;
        this.$data = options.data;
        // 根元素存在  编译模板
        if(this.$el){
            // 数据劫持
            new Observer(this.$data)
            // 数据代理
            this.proxyVm(this.$data)
            // 编译
            new Compiler(this.$el,this)
        }
    }
    proxyVm(data){
        for(let key in data){
            Object.defineProperty(this,key,{
                get(){
                    return data[key]
                }
            })
        }
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
   
</head>
<body>
    <div id="app">
        <input type="text" v-model="school.name" />
        <div>{{school.name}}</div>
        <div>{{school.age}}</div>
        <ul>
            <li>1</li>
            <li>1</li>
        </ul>
    </div>
     <script src="mvvm.js"></script>
     <script>
         let vm = new Vue({
             el:'#app',
             data:{
                 school:{
                     name:"名字",
                     age:18
                 }
             }
         })
     </script>
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值