美文网首页Vue
vue简版源码浅析

vue简版源码浅析

作者: 假面猿 | 来源:发表于2018-03-15 17:40 被阅读0次

    大家好,最近在看一些Vue的源码,由浅入深,先从最简单的说起吧。

    vue简版源码

    这款vue简版源码可以很好的帮我们理解mvvm的实现,源码只有四个js文件,我们来一起看一下:

    index.html 主页面
    index.js 入口文件
    observer.js 设置访问器及依赖收集
    compile.js dom编译
    watcher.js 依赖监听类

    接下来我们看一些核心代码(部分代码省略)
    index.html

    <div id="app">
      <h2>{{title}}</h2>
      <input v-model="name">
      <h1>{{name}}</h1>
      <button v-on:click="clickMe">click me!</button>
    </div>
    <script type="text/javascript">
      new Vue({
        el: '#app',
        data: {
          title: 'vue code',
          name: 'imooc',
        },
        methods: {
          clickMe: function () {
            this.title = 'vue code click'
          },
        },
        mounted: function () {
          window.setTimeout(() => {
            this.title = 'timeout 1000'
          }, 1000)
        },
      })
    </script>
    

    index.html文件中主要是html片段和vue的实例化,那么我们的html是怎么和vue关联起来的呢?数据变化是怎么影响html改变的呢?而页面改变又怎么更新到数据的呢?带着这两个问题,我们走进index.js

    index.js

    function Vue (options) {
      // 初始化
      var self = this
      this.data = options.data
      this.methods = options.methods
    
      Object.keys(this.data).forEach(function (key) {
        // 将this.a访问代理到this.data.a下(代码略)
        self.proxyKeys(key)  
      })
    
      observe(this.data) // 设置访问器及依赖收集
      new Compile(options.el, this) //dom编译与绑定监听
      options.mounted.call(this) // 所有事情处理好后执行 mounted 函数
    }
    

    在index.js中我写了一些注释,从注释中可以看到执行过程与文件的对应关系,那么我们的两个问题也有了思路:
    1.数据变化是怎么影响html改变的呢,在observe里找答案
    2.页面改变又怎么更新到数据的呢,在compile里找答案
    而解决上面两个问题,又离不开watcher的辅助,只有相互依赖互相监听,我们才能建立联系,OK,我们先从第一个问题下手:

    observe.js

    function observe (value, vm) {
      ...
      return new Observer(value)
    }
    function Observer (data) {
      this.data = data
      this.walk(data)
    }
    Observer.prototype = {
      walk: function (data) {
        var self = this
        Object.keys(data).forEach(function (key) {
          self.defineReactive(data, key, data[key])
        })
      },
      defineReactive: function (data, key, val) {
        var dep = new Dep()
        var childObj = observe(val)
        Object.defineProperty(data, key, {
          enumerable: true,
          configurable: true,
          get: function getter () {
            if (Dep.target) {
              dep.addSub(Dep.target)
            }
            return val
          },
          set: function setter (newVal) {
            if (newVal === val) {
              return
            }
            val = newVal
            dep.notify()
          },
        })
      },
    }
    
    function Dep () {
      this.subs = []
    }
    Dep.prototype = {
      addSub: function (sub) {
        this.subs.push(sub)
      },
      notify: function () {
        this.subs.forEach(function (sub) {
          sub.update()
        })
      },
    }
    Dep.target = null
    

    observe.js文件分为两个类,第一段代码是Observer用于属性访问器设置,第二段代码是Dep类于用依赖收集,核心的地方在于get set时对dep对象的处理。
    我们先来看Dep.target,这是一个静态属性,用于存放watch监听对象,定义在全局中,可见全局中只会有一个值,具体怎么用我们等下看watch。
    代码逻辑中判断是否有Dep.target,如果有就收集这个依赖,这个时候,我们可以大胆假设一下,对this.a进行get访问时,收集了什么依赖,然后在this.a = 1时,对收集的依赖进行了更新notify,这个什么依赖应该就是第一个问题的答案了吧,没错,就是对dom的监听。
    这样我们在observe中就看到了数据变化时触发dom监听去更新dom,第一个问题就有了答案,接下来我们为了优先把上面的疑问解开,先看watcher文件

    watcher.js

    function Watcher (vm, exp, cb) {
      this.cb = cb
      this.vm = vm
      this.exp = exp
      this.value = this.get() // 将自己添加到订阅器的操作
    }
    Watcher.prototype = {
      update: function () {
        this.run()
      },
      run: function () {
        var value = this.vm.data[this.exp]
        var oldVal = this.value
        if (value !== oldVal) {
          this.value = value
          this.cb.call(this.vm, value, oldVal)
        }
      },
      get: function () {
        Dep.target = this // 缓存自己
        var value = this.vm.data[this.exp] // 强制执行监听器里的 get 函数
        Dep.target = null // 释放自己
        return value
      },
    }
    

    在watcher文件中我们看到了一些熟悉的身影,Dep.targetupdate方法没错,这些是在observe中出现的,我们先看update,update方法是Dep类中notify调用的,notify是依赖通知,在update中我们又看到了cb回调函数。 看到这我们会想这个回调是什么,我们看Watcher的函数定义,第一个可以认为就是this,第二个是表达式(简单理解就是this.a),cb是回调函数,可见在实例化Watcher的时候,我们已经拿到了属性对应的回调,所以notify就是在通知属性对应的依赖触发,去做一些更新dom的事。那么notify出现在属性在重新赋值的地方也就顺理成章。

    接下来看Dep.target,在Watch的构造函数中有this.value = this.get() ,然后在get方法中Dep.target=当前的watcher对象(属性、回调),强制执行监听器里的 get 函数,达到两个效果,一watcher中保存了oldvalue,二执行get时,对应的属性会进行依赖收集,有没有印象if (Dep.target) ,所以这个时候,我们就完成了收集,最后Dep.target = null 释放,因为js单线程,所以此处定义为全局变量也没什么不可,毕竟收集上来的监听对象都收集到了闭包私有变量dep中,使每个data的属性都能对应自己的依赖。

    至此,第一个问题已经验证了很多回,那么dom改变如何影响数据改变呢?我们继续看compile.js

    compile.js

    function Compile (el, vm) {
      this.vm = vm
      this.el = document.querySelector(el)
      this.fragment = null
      this.init()
    }
    Compile.prototype = {
      init: function () {
        if (this.el) {
          this.fragment = this.nodeToFragment(this.el)
          this.compileElement(this.fragment)
          this.el.appendChild(this.fragment)
        } else {
          console.log('DOM 元素不存在')
        }
      },
      nodeToFragment: function (el) {
        var fragment = document.createDocumentFragment()
        var child = el.firstChild
        while (child) {
          // 将 DOM 元素移入 fragment 中
          fragment.appendChild(child)
          child = el.firstChild
        }
        return fragment
      },
      compileElement: function (el) {
        var childNodes = el.childNodes
        var self = this;
        [].slice.call(childNodes).forEach(function (node) {
          var reg = /\{\{(.*)\}\}/
          var text = node.textContent
    
          if (self.isElementNode(node)) {
            self.compile(node)
          } else if (self.isTextNode(node) && reg.test(text)) {
            self.compileText(node, reg.exec(text)[1])
          }
    
          if (node.childNodes && node.childNodes.length) {
            self.compileElement(node)
          }
        })
      },
      compile: function (node) {
        var nodeAttrs = node.attributes
        var self = this
        Array.prototype.forEach.call(nodeAttrs, function (attr) {
          var attrName = attr.name
          if (self.isDirective(attrName)) {
            var exp = attr.value
            var dir = attrName.substring(2)
            if (self.isEventDirective(dir)) {  // 事件指令
              self.compileEvent(node, self.vm, exp, dir)
            } else {  // v-model 指令
              self.compileModel(node, self.vm, exp, dir)
            }
            node.removeAttribute(attrName)
          }
        })
      },
      compileText: function (node, exp) {
        var self = this
        var initText = this.vm[exp]
        this.updateText(node, initText)
        new Watcher(this.vm, exp, function (value) {
          self.updateText(node, value)
        })
      },
      compileEvent: function (node, vm, exp, dir) {
        var eventType = dir.split(':')[1]
        var cb = vm.methods && vm.methods[exp]
    
        if (eventType && cb) {
          node.addEventListener(eventType, cb.bind(vm), false)
        }
      },
      compileModel: function (node, vm, exp, dir) {
        var self = this
        var val = this.vm[exp]
        this.modelUpdater(node, val)
        new Watcher(this.vm, exp, function (value) {
          self.modelUpdater(node, value)
        })
        node.addEventListener('input', function (e) {
          var newValue = e.target.value
          if (val === newValue) {
            return
          }
          self.vm[exp] = newValue
          val = newValue
        })
      },
      updateText: function (node, value) {
        node.textContent = typeof value === 'undefined' ? '' : value
      },
      modelUpdater: function (node, value, oldValue) {
        node.value = typeof value === 'undefined' ? '' : value
      },
      isDirective: function (attr) {
        return attr.indexOf('v-') == 0
      },
      isEventDirective: function (dir) {
        return dir.indexOf('on:') === 0
      },
      isElementNode: function (node) {
        return node.nodeType == 1
      },
      isTextNode: function (node) {
        return node.nodeType == 3
      },
    }
    

    comile代码较多,我们可以简化理解
    1.document.createDocumentFragment使用createDocumentFragment来将html映射为dom对象应该是1.X的方案,先不说这个,在2.X的文章中我们在讲虚拟dom吧
    2.核心方法compileElement,通过我们来分析每一个node节点的类型与内容,做不同的解析,比如{{a}}这里就存在一个监听,v-mode={{a}},这里又一个监听,最终跑完你会发现this.a更新赋值时,会有两个监听节点要更新,所以在html解析时,我们引入了watcher,传入对应data和回调,回调无疑问就是更新node节点。上面提到的逻辑就又验证了一遍。
    3.在这里我们回答第二个问题,dom更新时如何使data变化,举个简单的例子,在input框输入数值时,从代码可以看到node.addEventListener('input', function (e)),在这里我们直接对属性进行了赋值,从而更新了data。

    以上就是mvvm的简易实现,在此基础上我们就更好去解读Vue2.X的源码,篇幅较长,下一篇见啦~

    相关文章

      网友评论

        本文标题:vue简版源码浅析

        本文链接:https://www.haomeiwen.com/subject/qxnhqftx.html