简易版vue及源码,实现常用指令(插值表达式,v-text,v-html,v-model,@事件)数据响应式功能

简易版vue实现及源码(数据响应式及常用指令实现)

1. 简易vue工作机制分析

简易版vue重点实现以下两个功能:

  1. 数据响应式:改变data等中的数据,页面重新进行渲染;页面中的数据改变data中的数据同步发生改变
  2. 模板解析:提取模板中的插值表达式和指令,按照特定的语法进行解析,解析完成后插入el中,DOM进行重绘

实现分4个部分:Watcher类,Dep类,数据劫持,模板解析
本案例new Vue()初始化时主要做了3件事,保存options(vue配置)和data,进行数据拦截,创建compile实例

实际 new Vue() 时会调⽤用_init()进⾏行行初始化,会初始化各种实例例⽅方法、全局⽅方法、执⾏行行⼀一些⽣生命周期、初始化props、data等状态。其中最重要的是data的「响应化」处理。

  1. Watcher类:模板解析时,所有的插值表达式或指令都会创建一个watcher实例,每个watchers实例存放了其vue实例,插值表达式/指令,相应的模板替换方法
  2. Dep类:在每个data中的key开始进行数据劫持时,会创建一个dep实例,每个dep实例存放和当前key相同的watcher实例
  3. 数据拦截:getter时通知dep实例执行addDep方法,保存相同key值的watchers实例,setter时通知dep实例执行notify方法(重新更新DOM)
  4. 模板解析:使⽤用正则解析template中的vue的指令(v-xxx) 变量量等等

实际Vue编译模块分为三个阶段

  1. parse:使⽤用正则解析template中的vue的指令(v-xxx) 变量量等等 形成抽象语法树AST
  2. optimize:标记⼀些静态节点,⽤用作后⾯面的性能优化,在diff的时候直接略略过
  3. generate:把第⼀部生成的AST 转化为渲染函数 render function
    Virtual DOM 是react⾸首创,Vue2开始⽀支持,就是用 JavaScript 对象来描述dom结构,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff,最后再汇总所有的diff,力求做最少的dom操作,毕竟js里对比很快,而真实的dom操作太慢

vue工作机制分析

2.源码实现

1.数据响应式

本文中,重点实现data的「响应化」处理
细节:怎么将data挂载到vue实例上(源码中找答案)

property/Dep/Watcher对应关系图
property/Dep/Watcher对应关系

class KVue {
    constructor(options) {
        //保存vue配置
        this.$options = options;
        //保存data
        this.$data = options.data;
        //响应化处理
        this.observe(this.$data);
        new Compile(options.el, this);
        //钩子函数
        if (options.created) {
            options.created.call(this);
        }
    }
    observe(obj) {
        if (!obj || typeof obj !== 'object') {
            return
        }
        //观察data中每一个数据
        for (const key in obj) {
            this.defineReactive(obj, key, obj[key]);
            this.proxyData(key);
        }
    }
    defineReactive(obj, key, val) { //数据劫持,完成响应式
        //深层次遍历
        this.observe(key);
        let _val = val;
        //为data中的每个key创建dep实例
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            get() {
                // 依赖收集
                Dep.target && dep.addDep(Dep.target);
                return _val
            },
            set(newVal) {
                if (newVal !== val) {
                    _val = newVal;
                    dep.notify();
                }
            }
        })
    }
    // 在vue根上定义属性代理data中的数据
    proxyData(key) {
        // this指的KVue实例
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key];
            },
            set(newVal) {
                this.$data[key] = newVal;
            }
        });
    }
}
class Dep { //建立依赖类,
    constructor() {
        this.watchers = []
    }
    addDep(watcher) {
        this.watchers.push(watcher)
    }
    notify() {
        this.watchers.forEach(watcher => watcher.update())
    }
}
class Watcher { // 创建Watcher:保存data中数值和页面中的挂钩关系
    constructor(vm, key, cb) {
        // 创建实例时立刻将该实例指向Dep.target便于依赖收集
        this.vm = vm;
        this.key = key;
        this.cb = cb;

        //用来存放解析到插值或者指令及对应的vm实例
        Dep.target = this;
        this.vm[this.key];//触发依赖收集
        Dep.target = null;
    }
    // 更新
    update() {
        // console.log(this.key + "更新了!");
        this.cb.call(this.vm, this.vm[this.key])
    }
}

2.模板解析

编译过程图
编译过程图

//遍历dom结构,解析插值表达式或者指令
class Compile {
    constructor(el, vm) {
        this.$vm = vm;
        this.$el = document.querySelector(el);

        // 把模板中的内容移到片段操作
        this.$fragment = this.node2Fragment(this.$el);
        // 执行编译
        this.compile(this.$fragment);
        // 放回$el中
        this.$el.appendChild(this.$fragment);
    }
    node2Fragment(el) {
        // 创建元素片段
        const fragment = document.createDocumentFragment();
        let child;
        while ((child = el.firstChild)) {
            fragment.appendChild(child);
        }
        return fragment;
    }
    compile(el) {
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
            if (node.nodeType == 1) { //表示元素节点
                 console.log(`编译${node.nodeName}元素:`)
                // // console.log(node)
                //只编译v-xxx
                this.compileElement(node);
            } else if (this.isInter(node)) { //是否为插值表达式
                // //console.log(node)
                //编译{{}}
                this.compileText(node)
            }
            // 递归子节点
            if (node.children && node.childNodes.length > 0) {
                this.compile(node);
            }
        })
    }
    isInter(node) {
        return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }
    compileText(node) { //文本替换
        // 取出插值表达式内中的表达式
        const exp = RegExp.$1;
        this.update(node, exp, 'text')
    }
    update(node, exp, type) {
        const updator = this[type + "Updator"];
        updator && updator(node, this.$vm[exp]); // 首次初始化
        // 创建Watcher实例,收集依赖
        new Watcher(this.$vm, exp, function (val) {
            updator && updator(node, val);
        })
    }
    textUpdator(node, val) {
        console.log(node, val)
        //将插值表达式变为vm.$data的值
        node.textContent = val;
    }
    compileElement(node) {
        //只关心元素属性
        const nodeAttrs = node.attributes;
        console.log(node,nodeAttrs)
        Array.from(nodeAttrs).forEach(attr => {
            //k-xxx="aaa"
            const attrName = attr.name; //取出指令k-xxx
            const exp = attr.value; //取出指令值aaa
            console.log(attrName,exp)
            if (attrName.indexOf("k-") == 0) {
                // 指令
                const type = attrName.substring(2); //xxx
                // 执行
                this[type] && this[type](node, exp);
            } else if (attrName.indexOf('@') == 0) {
                //事件类型
                const type = attrName.substring(1);
                console.log(type);
                //执行
                this.handleEvent(node, this.$vm, exp, type)
            }
        })
    }
    handleEvent(node, vm, exp, type) {
        const fn = vm.$options.methods && vm.$options.methods[exp]
        if (type && fn) {
            node.addEventListener(type, fn.bind(vm))
        }
    }
    text(node, exp) { //k-text
        console.log(node, exp, 'k-text')
        this.update(node, exp, 'text')
    }
    html(node, exp) { //k-html
        this.update(node, exp, "html");
        console.log(node, exp, this.$vm[exp], 'k-html')  
    }
    htmlUpdator(node, val) {
        node.innerHTML = val;
    }
    model(node, exp) {//k-model
        this.update(node, exp, 'model')
        node.addEventListener('input', e => {
            this.$vm[exp] = e.target.value;
        })
    }
    modelUpdator(node, val) {
        node.value = val;
    }
}

测试代码

<body>
    <div id="app">
        {{name}}
        <p>{{name}}</p>
        <p k-text="name"></p>
        <p>{{age}}</p>
        <input type="text" k-model="name">
        <button @click="changeName">点我</button>
        <div k-html="html"></div>
    </div>
    <script src="./kvue.js"></script>
    <script src="./compile.js"></script>
    <script>
        const kvm = new KVue({
            el: '#app',
            data: {
                name: "I am test.",
                age: 12,
                html: '<button>这是一个按钮</button>'
            },
            created() {
                console.log('开始啦')
                setTimeout(() => {
                    this.name = '我是测试'
                }, 1500)
            },
            methods: {
                changeName() {
                    this.name = '哈喽,我来了'
                    this.age = 1
                }
            }
        })

    </script>
</body>

效果图:

结果created执行结果点击事件v-model
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

码云地址:
https://gitee.com/huang_canmin/VueExercise/tree/origin/02.%E6%BA%90%E7%A0%81/Vue%E5%AE%9E%E7%8E%B0

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值