从vue1到vue3及周边插件分析————手写简易vue1、及分析原理

408 阅读5分钟

vue到现在已经出到了3的版本,我们一步步回顾一下它的发展。以及分析核心原理。

logo.png

阅读本文需要一些es6基础,不太里了解的同学了可以看一下阮一峰大佬写的ES6入门教程

vue1是没有虚拟dom的,在这个版本我们只关注相应式。在vue1中实现数组双向绑定的是Object.defineProperty。
首先我们先做一个小demo新建一个index.js

function defineReactive(obj, key, val){
    Object.defineProperty(obj, key, {
        get (){
            console.log('get',val)
            return val
        },
        set (newVal){
            if(newVal !== val){
                console.log('set',newVal)
                val = newVal
            }
        }
    })
}


var  obj = {bar:123}
defineReactive(obj, bar, obj.bar)
obj.bar // 控制台打印get 123
obj.bar = 456 //控制台打印 set 456

图片

我看到这样就可以触发setter和getter。但是如果是对象嵌套对象呢?显然我们的方法是不满足这样的情况的。是否我们在调用之前看一下他的子项是什么呢?看一下这样是否能解决这个问题。

function defineReactive (obj, key, val) {
  observer(val) 
  // 如果是对象类型的重新执行监听更深一级
  Object.defineProperty(obj, key, {
    get () {
      console.log('get', val)
      return val
    },
    set (newVal) {
      if (newVal !== val) {
        console.log('set', newVal)
        val = newVal
        observer(val) 
        // 如果对象中的一项修改成数组重新监听一下
      }
    }
  })
}

function observer (obj) {
  if (typeof obj !== 'object' && obj != null) {
    return
  } // 如果传入的不是个对象返回不去执行
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}
var obj = { bar: 123, bar1: { a: 123 } }
observer(obj)
obj.bar1.a // get 123
obj.bar1.a = 465 // set 465

数据相应式

接下来我们看一下我们怎么使用vue

new Vue({
    el:'#app',
    data(){
        return {
            ...
        }
    },
    methods:{
        ...
    }
})

我们从调用这段代码可以分析出首先得需要一个vue的类(构造函数)然后传一个对象,对象中有这些属性el,data,methods。这些有可以说是我们使用这个类的时候需要的一个配置,一些可选配置。接下来我们来简单实现一个vue的类

class Vue{
    constructor(options){ // 传入的参数
        this.$options = options // 保存一下参数
        initData(this) // 执行初始化函数
    }
}

function initData(vm){
    let {data} = vm.$options // 将data单独取出来
    // 判断data是否存在
    if(!data){
        vm_data={} //如果data选项不存在就创建一个空的对象
    } else {
        vm_data = typeof === 'function' ? data() : data
        // 这里判断一下data是不是一个方法因为我们在写的时候可以直接data:{} 或者 data(){return {}} 俩个方式。如果是方法就执行一个返回一个对象。
    }
    // 这个方法的中重点来了,我们循环执行好的_data然后便利挂上代理,让我们可以直接访问到,而不是this.data.变量 直接 this.变量 这种形式
    Object.keys(vm._data).forEach(key => {
        proxy(vm, '_data', key)
    })
    observe(vm._data)
}
// 访问target.key 实际上是返回 target[sourceKey][key] 在这里相当于this.a 实际上访问的是this._data.a 同理当修改的时候也是改的目标上的值
function proxy(target, sourceKey, key){
    Object.defineProperty(target, key,{
        get() {
            return target[sourceKey][key]
        },
        set(newVal){
            target[sourceKey][key] = newVal
        }
    })
}

function observe(value){
    if(typeof value !== 'object' && value != null){
        return 
    }
    if(value.__ob__){
        return value.__ob__
    }
    new Observer(value)
}

class Observer{
    constructor(value){
        Object.defineProperty(value,'__ob__',{
            value:this,
            enumerable: false,
            writable: true,
            configurable: true
        })
        this.walk(value)
    }
    walk(obj){
        Object.keys(obj).forEach(key => {
          defineReactive(obj, key, obj[key])
        })
    }
}

  function defineReactive(obj, key, val) {
      observe(val)
      Object.defineProperty(obj, key, {
        get() {
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            val = newVal
            observe(val)
          }
        }
      })
    }

现在就可以实现一个没有关注视图的相应式的vue,在initData的时候我们也可以用同样的方法执行代理方法,props等等一些配置项。defineProperty只能给对象做代理,接下来我们给数组也做一下代理。

class Observer{
    constructor(value){
        Object.defineProperty(value, '__ob__', {
            value: this,
            enumerable: false,
            writable: true,
            configurable: true
        })
        if(Array.isArray(value)){
          // 处理数组、先预留位置
        } else {
            this.walk(value)
        }
    }
}

页面更新

上边我们在数据相应式完成的差不多了,现在要和页面结合在一起。 图片 在此之前我们要先了解这么几个东西
Vue: 框架构造函数
Observer: 执行数据相应化(分析数据是对象还是数组)
Complie:编译模板,初始化试图,收集依赖(更新函数、watcher创建)
Watcher:执行更新函数(更新DOM)
Dep:管理watcher,批量更新

编译模板-complie
编译模板中的vue特殊语法,初始化视图,更新函数

图片

class Compile{
    constructor(el, vm){
        this.$vm = vm
        this.$el = document.queryselector(el)
        if(this.$el){
            this.compile(this.$el)
        }
    }
    compile(el){
        el.childNodes.forEach(node =>{
            if(this.isElement(node)){
                this.compileElement(node)
            } else if(this.isInter(node)){
                this.complieText(node)
            }
            if(node.childNodes){
                this.compile(node)
            }
        })
    }
    
    isElement(node){
        return node.nodeType === 1
    }
    
    isInter(node){
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
    }
    
    complieText(node){
        this.update(node, RegExp.$1,'text')
    }
    
    compileElement(){
        // 获取节点属性
        const nodeAttrs = node.attributes
        Array.from(nodeAttrs).forEach(attr=>{
            const attrName = attr.name
            const exp = attr.value
            if(this.isDirective(attrName)){
                const dir = attrName.substring(2) //截取出v特殊vue指令 text html model
                this[dir] && this[dir](node,exp)
            } 
            if(this.isEvent(attrName)){
                const dir = attrName.substring(1)
                this.eventHandler(node, exp, dri)
            }
        })
    }
    
    eventHandler(node, exp, dir){
        const cb = this.$vm.$options.methods && this.$vm.$options.methods[exp]
        node.addEventListener(dir, fn.bind(this.$vm))
    }
    
    text(node, exp){
        this.update(node, exp, 'text')
    }
    
    html(node, exp){
        this.update(node, exp, 'html')
    }
    
    model(node, exp){
        this.update(node, exp, 'model')
        const { tagName, type } = node
        tagName = tagName.toLowerCase()
        if(tagName == 'input' && type == 'text'){
            // 如果绑定了初始值 赋值初始值
            node.value = this.$vm[exp]
            node.addEventListener('input',()=>{
                this.$vm[exp] = e.target.value
                // 监听text的输入事件改变绑定的值
            })
        }
        else if(tagName == 'input' && type == 'checkbox') {
            node.value = this.$vm[exp]
            node.addEventListener('change',()=>{
                this.$vm[exp] = e.target.checked
                // 监听checkbox的change事件改变绑定的值
            })
        }
        else if(tagName == 'select'){
            node.value = this.$vm[exp]
             node.addEventListener('input',()=>{
                this.$vm[exp] = e.target.value
                // 监听text的输入事件改变绑定的值
            })
        }
    }
    // 所有绑定都需要绑定更新函数以及对应wathcer实例
    update(node, exp, dir){
        const cb = this[dir + 'Updater']
        cb && cb(node, this.$vm(exp))
        new Watcher(this.$vm, exp, function(val){
            cb && cb(node, val)
        })
    }
    
    textUpdater(node, value){
        node.textContent = value
    }
    
    htmlUpdater(node, value){
         node.innerHTML = value
    }
    
    modelUpadter(node, value){
        node.value = value
    }
     
}

vue1中key和dep是一一对应的data中返回的对象对应一个dep,对象中每个key都对应这一个dep

class Dep{
   constructor(){
    this.watchers = []
  }
    static target = null
    depend(){
        if (this.watchers.includes(Dep.target)){
            return
        }
        this.watchers.push(Dep.target)
    }
    notify(){
        this.watchers.forEach(watcher=>{
            watcher.update()
        })
    }
}

页面中一个依赖对应着一个watcher,在vue1中dep和watcher是1:n的关系

class watcher{
    constructor(vm, key, updateFn){
        this.vm = vm
        this.key = key
        this.updateFn = updateFn
        
        // 读一次数据,触发definrReaective里面的get()
        Dep.target = this
        this.vm[this.key]
        Dep.target = null
    }
    
    update(){
        // 传入当亲的最新值给更新函数 
    this.updateFn.call(this.vm, this.vm[this.key])
    }
    
}

这样就基本上形成了一个相应式的页面,但是我们在重写Array方法是还差一点内容,我们只复制了方法没有通知更新。下面我们来继续完善这个方法。

    const orignalProto = Array.prototype;
    const arrayProto = Object.create(orignalProto);
    // 只有这个7个方法会改变原数组所以我们重写这7个方法
    ['push','pop','shift','unshift','splice','reverse','sort'].forEach(method =>{
        orignalProto[method].apply(this, arguments)
        let inserted = []
        switch(method) {
            case 'push':
            case 'unshift':
                inserted = arguments
                break;
            case 'splice':
                inserted = arguments.slice(2)
                break;
        }
        if(inserted.length > 0){
            inserted.forEach(key=>{
                observe(key)
            })
        }
    })
    

一个简易的vue就完成了,我们只做了最简单的分析代码,没有做兼容性处理。千万不要去较真。

总结

vue1在设计之初为了解决我们日常繁琐的获取dom,获取值,监听dom变化,更新dom数据等。他把上述的弊端汇总形成一个自动化的框架,减少我们繁琐的操作。
当然vue1也是有缺点的,vue1中没有虚拟dom和Diff在项目体积庞大内容丰富的时候会消耗很多资源。页面每有一个相应式数据就会产生一个watcher,在大型页面中会有非常多的数据同样也会产生非常多的watcher。总用资源。