创立工夫:2020-09-11

在线演示demo


次要了解、实现如下办法:

  • Observe :监听器 监听属性变动并告诉订阅者
  • Watch :订阅者 收到属性变动,更新视图
  • Compile :解析器 解析指令,初始化模板,绑定订阅者,绑定事件
  • Dep :寄存对应的所有 watcher 实例
次要执行流程

将watcher装入对应的dep实例的订阅列表中的过程

相干html代码,用于被解析绑定数据

这里的代码即是compile中要解析的模板,解析其中的 v-model {{}} v-on 等命令

<div id="app">    <p> 姓名:<input type="text" v-model="name"></p>    <p>学号:<input type="text" v-model="number"></p>    <p><span>学号:</span> <span>{{ number }}</span></p>    <p><span>computed实现:</span> <span>{{ getStudent }}</span></p>    <p>        <button v-on:click="setData">事件绑定</button>    </p></div>

observer代码

为data数据增加 get、set 以便增加 watcher,和创立 Dep 实例,告诉更新视图

const defineProp = function(obj, key, value) {    observe(value)    /*    * 事后为每一个不同属性创立一个独有的dep    */    const dep = new Dep()    Object.defineProperty(obj, key, {        get: function() {            /*            * 依据不同的属性创立且只在创立Watcher时调用            */            if(Dep.target) {                dep.targetAddDep()            }            return value        },        set: function(newVal) {            if(newVal !== value) {                /*                * 这里的赋值操作,是以便于get  办法中返回value,因为都是赋值后马上就会调用get办法                */                value = newVal                /*                * 告诉监听的属性的所有订阅者                */                dep.notify()            }        }    })}const observe = function(obj) {    if(!obj || typeof obj !== 'object') return    Object.keys(obj).forEach(function(key) {        defineProp(obj, key, obj[key])    })}

Dep代码

次要是将 watcher 放入 对应的 dep 订阅列表

let UUID = 0function Dep() {    this.id = UUID++    // 寄存以后属性的所有的监听watcher    this.subs = []}Dep.prototype.addSub = function(watcher) {    this.subs.push(watcher)}// 目标是将以后 dep 实例 传入 watcherDep.prototype.targetAddDep = function() {    // 这里 this 是实例化后的 dep    Dep.target.addDep(this)}Dep.prototype.notify = function() {    // 触发以后属性的所有 watcher    this.subs.forEach(_watcher => {        _watcher.update()    })}Dep.target = null

Watcher 代码

数据更新后,更新视图

function Watcher(vm, prop, callback) {    this.vm = vm    this.prop = prop    this.callback = callback    this.depId = {}    this.value = this.pushWatcher()}Watcher.prototype = {    update: function() {        /* 更新值的变动 */        const value = this.vm[this.prop]        const oldValue = this.value        if (value !== oldValue) {            this.value = value            this.callback(value)        }    },    // 目标是接管 dep 实例,用于将以后watcher实例放入 subs    addDep: function(dep) {        if(!this.depId.hasOwnProperty(dep.id)) {            dep.addSub(this)            this.depId[dep.id] = dep.id        } else {            console.log('already exist');        }    },    pushWatcher: function() {        // 存贮订阅器        Dep.target = this        // 触发对象的get监听,将下面赋值给 target 的this 退出到subs        var value = this.vm[this.prop]        // 退出完后就删除        Dep.target = null        return value    }}

Compile 代码

解析html模板,创立代码片段,绑定数据事件

function Compile(vm) {    this._vm = vm    this._el = vm._el    this.methods = vm._methods    this.fragment = null    this.init()}Compile.prototype = {    init: function() {        this.fragment = this.createFragment()        this.compileNode(this.fragment)        // 当代码片段中的内容编译完了之后,插入DOM中        this._el.appendChild(this.fragment)    },    // 依据实在的DOM节点,创立文档片段    createFragment: function() {        const fragment = document.createDocumentFragment()        let child = this._el.firstChild        while(child) {            // 将节点退出到文档片段中后,该节点会被从原来的地位删除,相当于挪动节点地位            fragment.appendChild(child)            child = this._el.firstChild        }        return fragment    },    compileNode: function(fragment) {        let childNodes = fragment.childNodes;        [...childNodes].forEach(node =>{            if(this.isElementNode(node)) {                this.compileElementNode(node)            }            let reg = /\{\{(.*)\}\}/            // 获取节点下的所有文本内容            let text = node.textContent            // 判断是否已是纯文本节点,且文本内容是否有{{}}            if(this.isTextNode(node) && reg.test(text)) {                let prop = reg.exec(text)[1].trim()                this.compileText(node, prop)            }            if(node.childNodes && node.childNodes.length) {                // 递归编译子节点                this.compileNode(node)            }        })    },    compileElementNode: function(element) {        // 获取属性,只有元素节点有如下办法        let nodeAttrs = element.attributes;        [...nodeAttrs].forEach(attr => {            let name = attr.name            if(this.isDirective(name)) {                /*                * v-model 放在可承受input事件的标签上                */                let prop = attr.value                if (name === 'v-model') {                    /*                    * 获取到的value 即为须要绑定的data                    */                    this.compileModel(element, prop)                } else if(this.isEvent(name)) {                    /*                    * 绑定事件                    */                    this.bindEvent(element, name, prop)                }            }        })    },    compileModel: function(element, prop) {        let val = this._vm[prop]        this.updateElementValue(element, val)        new Watcher(this._vm, prop, value => {            this.updateElementValue(element, value)        })        element.addEventListener('input', event => {            let newValue = event.target.value            if (val === newValue) return            this._vm[prop] = newValue        })    },    compileText: function(textNode, prop) {        let text = ''        if(/\./.test(prop)) {            var props = prop.split('.')            text = this._vm[props[0]][props[1]]        } else {            text = this._vm[prop]        }        this.updateText(textNode, text)        console.log(text);        new Watcher(this._vm, prop, (value) => {            this.updateText(textNode, value)        })    },    bindEvent: function(element, name, prop) {        var eventType = name.split(':')[1]        var fn = this._vm._methods[prop]        element.addEventListener(eventType, fn.bind(this._vm))    },    /*    * 判断属性是否为指令    */    isDirective: function (text) {        return /v-/.test(text)    },    isEvent: function(text) {        return /v-on/.test(text)    },    isElementNode: function(node) {        // 元素节点返回1 文本节点(元素或属性中的文字)3 属性节点2(被废除)        return node.nodeType === 1    },    isTextNode: function(node) {        return node.nodeType === 3    },    updateElementValue: function(element, value) {        element.value = value || ''    },    updateText: function(textNode, value) {        textNode.textContent = value || ''    }}

vue 简要构造函数

次要实现了数据的双向绑定,自定义事件,computed

function FakeVue(options) {    this._data = options.data    this._methods = options.methods    this._computed= options.computed    this._el = document.querySelector(options.el)    // 将 _data 中的属性代理到外层的vm上,这里只代理了_data的第一层属性    Object.keys(this._data).forEach(key => {        this._proxyData(key)    })    this._initComputed()    this._init()}FakeVue.prototype._init = function() {    // 开始递归监听对象的所有属性,直到属性值为值类型    observe(this._data)    new Compile(this)}FakeVue.prototype._proxyData = function(key) {    Object.defineProperty(this, key, {        get: function() {            return this._data[key]        },        set: function (value) {            this._data[key] = value        }    })}FakeVue.prototype._initComputed = function() {    // 简略的实现: 将值挂载到跟上即可    const computed = this._computed    Object.keys(computed).forEach((v) => {        Object.defineProperty(this, v, {            get: computed[v],            set: function (value) {}        })    })}

创立vue实例

try{    let vm = new FakeVue({        el: '#app',        data: {            name: 'warren',            number: '10011',            score: {                math: 90            }        },        computed: {            getStudent: function() {                return `${this.name}:学号是 ${this.number}`            }        },        methods:{            // 通过在compile中给元素绑定事件实现            setData: function() {                alert('name:'+this.name);            }        }    });} catch(error) {    console.error(error)}

结语

这是从作者的了解角度,论述的一个简略的vue实现原理示例,心愿对正在摸索的你有所帮忙

在这个示例中,次要的简单点在于对 html 模板的解析,数据的双向绑定。

倡议跟着代码的执行程序理解整个过程,关键点的代码都有必要的正文,若发现问题请斧正。

最初附上 vue 源码地址,次要关注其中的 corecompiler 文件;

欢送交换 Github