为什么要从 Second Commit 开始阅读呢?

  • 答案来自我看过我的一篇博客,但是关于 First Commit,基本是关于 Grunt 的配置,现代前端开发中,多以使用 Webpack 为主,所以粗略看了一下之后并无记录的打算。

不罗嗦,直接看代码

在这段代码我们已经触及了 Vue 的核心:data bind,实际上也跟我们现在写的 Vue 差不多(当然,这里不是指 SFC )

首先在 HTML 部分,我们看到了文档中的“Mustache”语法,接下来我们看看最初的 data bind 是如何实现的。

在第 23 行,var content = el.innerHTML.replace(/\{\{(.*)\}\}/g, markToken),用正则表达式匹配{{...}}

关于 String.prototype.replace()

你可以指定一个函数作为第二个参数。在这种情况下,当匹配执行后,该函数就会执行。 函数的返回值作为替换字符串。 (注意:上面提到的特殊替换参数在这里不能被使用。) 另外要注意的是,如果第一个参数是正则表达式,并且其为全局匹配模式,那么这个方法将被多次调用,每次匹配都会被调用。

当第二个参数是一个函数时,函数接收多个参数,第一个参数match,表示匹配的子串,比如:{{msg}}{{what}}等,第二个参数用于匹配正则表达式中括号匹配到的字符串,就是匹配正则表达式的这一部分:(.*),并且这里可以接收多个参数:p1,p2,p3...,他们分别是括号对应的索引,具体参见 MDN。

ok,我们继续回到 23 行,每次匹配执行后都会执行这个函数,这个函数首先会往bindings内填充key:value,key 是{{}}内的值,value 则是一个空对象,然后 return 一个字符串 return '<span ' + bindingMark + '="' + variable +'"></span>'

接着遍历 bindings 执行 bind 方法

function bind (variable) {
  bindings[variable].els = el.querySelectorAll('[' + bindingMark + '="' + variable + '"]')
    ;[].forEach.call(bindings[variable].els, function (e) {
        e.removeAttribute(bindingMark)
    })
  Object.defineProperty(data, variable, {
    set: function (newVal) {
        [].forEach.call(bindings[variable].els, function (e) {
            bindings[variable].value = e.textContent = newVal
        })
    },
    get: function () {
        return bindings[variable].value
    }
  })
}

首先会找到含有bindingMark=... Attribute的 Element,并在bindings对应的key中记录它,接着再删除元素中的bindingMark Attribute,最后使用Object.defineProperty来做到 data bind,这也是 Vue 数据响应的核心。

接着遍历initData,将initData中的值浅拷贝给data,上面已经使用Object.defineProperty定义了data对于{{}}中值的getset。所以此时将initData中的键值对拷贝到data中,就会触发对应 property 的set,这里也就完成了对模板的数据更新。

这里已经包含了 data bind 和数据响应的核心,当然,这只是一个简单的实现。此外,Vue 的另一个核心:component system 还没有出现,后面会慢慢看到。

有兴趣的同学可以听一下尤小右当初为什么要开发 Vue 的 podcast,很有意思,在这里你能很快找到 Vue 的核心并且理解为什么需要它们。