手写一个简易Vue

1. 起步

1.1 响应式原理

我们都知道响应式是vue最独特的特性,是非入侵的响应式系统.数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

下面是vue响应式原理的简易图示:

index.png

在vue官网,深入响应式原理中, 它是这么描述的:

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

1.2原理分析

  1. Vue类负责初始化,将data中的属性注入到Vue实例中,并调用Observer类和Complier类,对数据进行劫持和解析,
  2. Observer类负责数据劫持, 通过Object,definePrototype,实现每一个data转换为gettersetter.
  3. Compiler负责解析指令和编译模板,初始化视图,收集依赖, 更新视图.
  4. Dep类负责收集依赖.添加观察者模式.通知data对应的所有观察者watcher来更新视图.
  5. Watcher类负责数据更新后,使关联视图重新渲染(更新DOM).

2. 开始

2.1 Vue类

class Vue {
    constructor(options) {
        // 限定Options类型为对象, 如果类型不为对象, 就抛出一个错误
        if (!(options instanceof Object)) throw new TypeError('The parameter type should be an Object');
        // 保存options中的数据
        this.$options = options || {};
        this.$data = options.data || {};
        this.$el = options.el;
        // 将vue实例中的data属性转换为 getter和setter, 并注入到vue实例中, 方便调用vm.msg
        this._proxyData(this.$data)
        //调用Observer类, 进行数据监听
        new Observer(this.$data)
        // 如果el元素有值, 调用Compiler类, 解析指令和插值表达式
        if (this.$el) new Compiler(this.$el, this)

    }
    _proxyData(data) {
        // 遍历data属性的key, 利用Object,definePrototype 进行数据劫持
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                set(newVal) {
                    if (newVal !== data[key]) data[key] = newVal;
                },
                get() {
                    return data[key]
                }
            })
        })
    }
}

2.2 Observer类

class Observer {
    constructor(data) {
        this.observe(data)
    }
    observe(data) {
        // 如果设置的数据类型为对象就设置为响应式数据
        if (data && typeof data === 'object') {
            Object.keys(data).forEach(key => {
                //调用设置响应式数据的方法
                this.definePReactive(data, key, data[key])
            })
        };
    }
    // 设置属性为响应式数据
    definePReactive(obj, key, value) {
        // 利递归使深层属性转换为 响应式数据
        this.observe(value)
        const that = this; // 保存内部this, 方便内部调用
        // 负责收集依赖并发送通知
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                // 订阅数据变化时, 往Dep中添加观察者, 收集依赖;
                Dep.target && dep.addSub(Dep.target);
                return value
            },
            set(newVal) {
                that.observe(newVal)
                if (newVal !== value) {
                    //如果新设置的值也为对象, 也转换为响应式数据
                    value = newVal;
                }
                 // 发送通知;
                dep.notify()
            }
        })
    }
}

2.3 Compiler类

class Compiler { // 解析指令, 编译模板
    static compileUtil = {// 解析指令的对象
        getVal(key, vm) {// 获取指令上的数据值
            //利用reduce 获取实例对象深层的属性值 
            return key.split('.').reduce((data, current) => {
                return data[current]
            }, vm.$data)
        },
        // 改变实例上的数据
        setVal(key, vm, inputVal) {// key 属性值, vm: vue实例对象, inputVal: 输入框的值
            let total = 'vm';// 用于拼接 出 vm['person']['name'] = inputVal
            if (key.split('.').length === 1) vm[key] = inputVal;
            else {
                // 对字符串进行拼接
                key.split('.').forEach(k => total += `['${k}']`)
                total += '=' + `${'inputVal'}`
                eval(total) // 利用eval强行将字符串解析为函数执行
            }
        },
        text(node, key, vm) { // 编译v-text指令的方法
            let value;// 保存获取的数据值
            if (/\{\{(.+?)\}\}/.test(key)) {
                // 全局匹配{{}}里面的变量, 利用...运算符展开 匹配的内容
                // 利用正则解析出{{xxx.xx}}中的变量, 并取出相应的变量值
                value = key.replace(/\{\{(.+?)\}\}/, (...args) => {
                    // 创建watcher对象, 当数据改变时, 更新视图
                    new Watcher(vm, args[1], newVal => { // 接受callback执行时第一个参数
                        this.updater.textUpdater(node, newVal)
                    })
                    return this.getVal(args[1], vm);
                })
            } else {
                // 获取key 对应的数据
                value = this.getVal(key, vm);
            }
            // 更新视图
            this.updater.textUpdater(node, value)
        },
        model(node, key, vm) {// 解析v-model 指令
            const value = this.getVal(key, vm);
            /// 数据 => 视图 
            new Watcher(vm, key, newVal => {
                this.updater.modelUpdater(node, newVal)
            })
            // 视图 => 数据 => 视图 双向数绑定
            node.addEventListener('input', e => {
                this.setVal(key, vm, e.target.value)
            })
            this.updater.modelUpdater(node, value)
        },
        html(node, key, vm) {// 解析HTML指令
            const value = this.getVal(key, vm);
            new Watcher(vm, key, newVal => {
                this.updater.htmlUpdater(node, newVal)
            })
            this.updater.htmlUpdater(node, value)
        },
        on(node, key, vm, eventName) {// 解析v-on:click指令
            // 获取实例对象中的methods中的方法
            const fn = vm.$options.methods && vm.$options.methods[key];
            // 绑定事件
            node.addEventListener(eventName, function (ev) {
                fn.call(vm, ev)// 改变fn函数内部的this,并传递事件对象event
            }, false)
        },
        bind(node, key, vm, AttrName) {// 解析 v-bind 指令
            node[AttrName] = vm.$data[key]
        },
        updater: {// 保存所有更新页面视图的方法的对象
            textUpdater(node, value) {
                node.textContent = value
            },
            htmlUpdater(node, value) {
                node.innerHTML = value
            },
            modelUpdater(node, value) {
                node.value = value
            }
        }
    }
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 获取文档碎片, 减少页面的回流和重绘
        const fragment = this.nodeFragment(this.el);
        // 编译文档碎片
        this.compile(fragment)
        // 追加到根元素
        this.el.appendChild(fragment)
    }
    nodeFragment(el) {
        // 创建文档碎片
        const fragment = document.createDocumentFragment();
        // 如果当前第一个子节点有值, 追加到文档碎片
        while (el.firstChild) {
            fragment.appendChild(el.firstChild)
        }
        return fragment
    }
    compile(fragment) {
        // 获取子节点
        const childNodes = fragment.childNodes;
        // 遍历所的子节点
        [...childNodes].forEach(child => {
            // 如果为元素节点
            if (this.isElementNode(child)) {
                this.compileElement(child)
            } else {
                // 解析文本节点
                this.compileText(child)
            }
            // 如果子节点还有子节点元素就递归遍历该子节点
            if (child.childNodes && child.childNodes.length) {
                this.compile(child)
            }
        })
    }
    compileElement(node) { // 编译元素节点
        const {compileUtil} = Compiler;
        // 获取元素节点的所有自定义属性
        const attributes = node.attributes;
        //利用展开运算符将attributes类数组对象转换为数组并遍历
        [...attributes].forEach(attr => {
            //将v-mode=msg 中的 v-model 和 msg 解构出来
            const {name, value} = attr;
            //判断属性是否为 v-开头
            if (this.isDirective(name)) {
                // 解构出v-text 中的 text
                const [, directive] = name.split('-');
                // 解构出 v-on:click 中的 on 和 click
                const [dirname, eventName] = directive.split(':');
                // 利用策略组合模式 调用相应的解析方法并 更新数据 数据驱动视图
                compileUtil[dirname](node, value, this.vm, eventName)
                // 删除有指令的标签上的属性
                node.removeAttribute('v-' + directive)
            }
        })
    }
    compileText(node) { // 编译文本节点
        const text = node.textContent;
        // 把Compiler类中的compileUtil对象解构出来
        const {  compileUtil} = Compiler;
        let reg = /\{\{(.+?)\}\}/;// 匹配 {{xx.xx}}的正则
        if (reg.test(text)) { // 如果是 {{}}的文本节点
            compileUtil['text'](node, text, this.vm)
        }
    }
    isDirective(attrName) { // 是否为v-开头的指令
        return attrName.startsWith('v-')
    }
    isElementNode(node) { // 是否为 元素节点
        return node.nodeType === 1;
    }
}

2.4 Dep类

class Dep {
    constructor() {
        // 保存所有的观察者列表
        this.subs = []
    }
    addSub(sub) { // 收集观察者
        this.subs.push(sub)
    }
    notify() { // 通知观察者就更新视图
        this.subs.forEach(w => w.update())
    }
}

2.5 Watcher类

class Watcher {
    constructor(vm, key, callback = value => {}) {
        this.vm = vm;
        // data中的属性名
        this.key = key;
        //回调函数负责更新视图
        this.callback = callback;
        //先把旧值保存起来;
        this.oldValue = this.getOldValue()
    }
    getOldValue() {
        // 把Watcher对象挂载到 Dep类的静态属性 target中
        Dep.target = this;
        const oldVal = Compiler.compileUtil.getVal(this.key, this.vm);
        // 清空watcher对象,避免重复设置
        Dep.target = null; 
        return oldVal
    }
    // 当数据发生改变时更新视图
    update() {
        const newVal = Compiler.compileUtil.getVal(this.key, this.vm);
        if (newVal !== this.oldValue) {
            //当数据发生改变调用callback并传递新值
            this.callback(newVal)
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343