JavaScript 实现一个简单的 Vue

2018年9月20日 288点热度 0人点赞 0条评论

vue的使用相信大家都很熟练了,使用起来简单。但是大部分人不知道其内部的原理是怎么样的,今天我们就来一起实现一个简单的vue。

Object.defineProperty()

实现之前我们得先看一下Object.defineProperty的实现,因为vue主要是通过数据劫持来实现的,通过 getset来完成数据的读取和更新。

  1. var obj = {name:'wclimb'}

  2. var age = 24

  3. Object.defineProperty(obj,'age',{

  4.    enumerable: true, // 可枚举

  5.    configurable: false, // 不能再define

  6.    get () {

  7.        return age

  8.    },

  9.    set (newVal) {

  10.        console.log('我改变了',age +' -> '+newVal);

  11.        age = newVal

  12.    }

  13. })

  14. > obj.age

  15. > 24

  16. > obj.age = 25;

  17. > 我改变了 24 -> 25

  18. > 25

从上面可以看到通过 get获取数据,通过 set监听到数据变化执行相应操作,还是不明白的话可以去看看Object.defineProperty文档。

流程图

图片

html 代码结构

  1. <div id="wrap">

  2.    <p v-html="test"></p>

  3.    <input type="text" v-model="form">

  4.    <input type="text" v-model="form">

  5.    <button @click="changeValue">改变值</button>

  6.    {{form}}

  7. </div>

js 调用

  1.    new Vue({

  2.        el: '#wrap',

  3.        data:{

  4.            form: '这是form的值',

  5.            test: '<strong>我是粗体</strong>',

  6.        },

  7.        methods:{

  8.            changeValue(){

  9.                console.log(this.form)

  10.                this.form = '值被我改变了,气不气?'

  11.            }

  12.        }

  13.    })

Vue 结构

  1.    class Vue{

  2.        constructor(){}

  3.        proxyData(){}

  4.        observer(){}

  5.        compile(){}

  6.        compileText(){}

  7.    }

  8.    class Watcher{

  9.        constructor(){}

  10.        update(){}

  11.    }

  • Vueconstructor 构造函数主要是数据的初始化

  • proxyData 数据代理

  • observer 劫持监听所有数据

  • compile 解析dom

  • compileText 解析 dom里处理纯双花括号的操作

  • Watcher 更新视图操作

Vue constructor 初始化

  1.    class Vue{

  2.        constructor(options = {}){

  3.            this.$el = document.querySelector(options.el);

  4.            let data = this.data = options.data;

  5.            // 代理data,使其能直接this.xxx的方式访问data,正常的话需要this.data.xxx

  6.            Object.keys(data).forEach((key)=> {

  7.                this.proxyData(key);

  8.            });

  9.            this.methods = obj.methods // 事件方法

  10.            this.watcherTask = {}; // 需要监听的任务列表

  11.            this.observer(data); // 初始化劫持监听所有数据

  12.            this.compile(this.$el); // 解析dom

  13.        }

  14.    }

上面主要是初始化操作,针对传过来的数据进行处理。

proxyData 代理 data

  1. class Vue{

  2.        constructor(options = {}){

  3.            ......

  4.        }

  5.        proxyData(key){

  6.            let that = this;

  7.            Object.defineProperty(that, key, {

  8.                configurable: false,

  9.                enumerable: true,

  10.                get () {

  11.                    return that.data[key];

  12.                },

  13.                set (newVal) {

  14.                    that.data[key] = newVal;

  15.                }

  16.            });

  17.        }

  18.    }

上面主要是代理 data到最上层, this.xxx的方式直接访问 data

observer 劫持监听

  1. class Vue{

  2.        constructor(options = {}){

  3.            ......

  4.        }

  5.        proxyData(key){

  6.            ......

  7.        }

  8.        observer(data){

  9.            let that = this

  10.            Object.keys(data).forEach(key=>{

  11.                let value = data[key]

  12.                this.watcherTask[key] = []

  13.                Object.defineProperty(data,key,{

  14.                    configurable: false,

  15.                    enumerable: true,

  16.                    get(){

  17.                        return value

  18.                    },

  19.                    set(newValue){

  20.                        if(newValue !== value){

  21.                            value = newValue

  22.                            that.watcherTask[key].forEach(task => {

  23.                                task.update()

  24.                            })

  25.                        }

  26.                    }

  27.                })

  28.            })

  29.        }

  30.    }

同样是使用 Object.defineProperty来监听数据,初始化需要订阅的数据。

把需要订阅的数据 pushwatcherTask里,等到时候需要更新的时候就可以批量更新数据了。?下面就是遍历订阅池,批量更新视图:

  1.    set(newValue){

  2.        if(newValue !== value){

  3.            value = newValue

  4.            // 批量更新视图

  5.            that.watcherTask[key].forEach(task => {

  6.                task.update()

  7.            })

  8.        }

  9.    }              

compile 解析 dom

  1. class Vue{

  2.        constructor(options = {}){

  3.            ......

  4.        }

  5.        proxyData(key){

  6.            ......

  7.        }

  8.        observer(data){

  9.            ......

  10.        }

  11.        compile(el){

  12.            var nodes = el.childNodes;

  13.            for (let i = 0; i < nodes.length; i++) {

  14.                const node = nodes[i];

  15.                if(node.nodeType === 3){

  16.                    var text = node.textContent.trim();

  17.                    if (!text) continue;

  18.                    this.compileText(node,'textContent')                

  19.                }else if(node.nodeType === 1){

  20.                    if(node.childNodes.length > 0){

  21.                        this.compile(node)

  22.                    }

  23.                    if(node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')){

  24.                        node.addEventListener('input',(()=>{

  25.                            let attrVal = node.getAttribute('v-model')

  26.                            this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'value'))

  27.                            node.removeAttribute('v-model')

  28.                            return () => {

  29.                                this.data[attrVal] = node.value

  30.                            }

  31.                        })())

  32.                    }

  33.                    if(node.hasAttribute('v-html')){

  34.                        let attrVal = node.getAttribute('v-html');

  35.                        this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML'))

  36.                        node.removeAttribute('v-html')

  37.                    }

  38.                    this.compileText(node,'innerHTML')

  39.                    if(node.hasAttribute('@click')){

  40.                        let attrVal = node.getAttribute('@click')

  41.                        node.removeAttribute('@click')

  42.                        node.addEventListener('click',e => {

  43.                            this.methods[attrVal] && this.methods[attrVal].bind(this)()

  44.                        })

  45.                    }

  46.                }

  47.            }

  48.        },

  49.        compileText(node,type){

  50.            let reg = /\{\{(.*?)\}\}/g, txt = node.textContent;

  51.            if(reg.test(txt)){

  52.                node.textContent = txt.replace(reg,(matched,value)=>{

  53.                    let tpl = this.watcherTask[value] || []

  54.                    tpl.push(new Watcher(node,this,value,type))

  55.                    if(value.split('.').length > 1){

  56.                        let v = null

  57.                        value.split('.').forEach((val,i)=>{

  58.                            v = !v ? this[val] : v[val]

  59.                        })

  60.                        return v

  61.                    }else{

  62.                        return this[value]

  63.                    }

  64.                })

  65.            }

  66.        }

  67.    }

这里代码比较多,我们拆分看你就会觉得很简单了。

首先我们先遍历 el元素下面的所有子节点, node.nodeType===3 的意思是当前元素是文本节点, node.nodeType===1 的意思是当前元素是元素节点。因为可能有的是纯文本的形式,如 纯双花括号就是纯文本的文本节点,然后通过判断元素节点是否还存在子节点,如果有的话就递归调用 compile方法。下面重头戏来了,我们拆开看:

  1. if(node.hasAttribute('v-html')){

  2.    let attrVal = node.getAttribute('v-html');

  3.    this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML'))

  4.    node.removeAttribute('v-html')

  5. }

上面这个首先判断node节点上是否有 v-html这种指令,如果存在的话,我们就发布订阅,怎么发布订阅呢?只需要把当前需要订阅的数据 pushwatcherTask里面,然后到时候在设置值的时候就可以批量更新了,实现双向数据绑定,也就是下面的操作:

  1. that.watcherTask[key].forEach(task => {

  2.    task.update()

  3. })

然后 push的值是一个 Watcher的实例,首先他new的时候会先执行一次,执行的操作就是去把 纯双花括号-> 1,也就是说把我们写好的模板数据更新到模板视图上。

最后把当前元素属性剔除出去,我们用 Vue的时候也是看不到这种指令的,不剔除也不影响。

至于 Watcher是什么,看下面就知道了。

Watcher

  1. class Watcher{

  2.    constructor(el,vm,value,type){

  3.        this.el = el;

  4.        this.vm = vm;

  5.        this.value = value;

  6.        this.type = type;

  7.        this.update()

  8.    }

  9.    update(){

  10.        this.el[this.type] = this.vm.data[this.value]

  11.    }

  12. }

之前发布订阅之后走了这里面的操作,意思就是把当前元素如: node.innerHTML = '这是data里面的值'、 node.value = '这个是表单的数据'。

那么我们为什么不直接去更新呢,还需要 update做什么,不是多此一举吗?其实 update记得吗?我们在订阅池里面需要批量更新,就是通过调用 Watcher原型上的 update方法。

效果

在线效果地址:http://www.wclimb.site/myVue,大家可以浏览器看一下效果,由于本人太懒了, gif效果图就先不放了,哈哈??

完整代码

完整代码已经放到 github上了 -> https://github.com/wclimb/MyVue

参考

  • 剖析Vue原理&实现双向绑定MVVM

  • 仿Vue实现极简双向绑定


欢迎关注 SegmentFault 微信公众号 :)

图片

8990JavaScript 实现一个简单的 Vue

root

这个人很懒,什么都没留下

文章评论