vue简版源码浅析

大家好,最近在看一些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的源码,篇幅较长,下一篇见啦~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容