为什么要从 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
对于{{}}
中值的get
和set
。所以此时将initData
中的键值对拷贝到data
中,就会触发对应 property 的set
,这里也就完成了对模板的数据更新。
这里已经包含了 data bind 和数据响应的核心,当然,这只是一个简单的实现。此外,Vue 的另一个核心:component system 还没有出现,后面会慢慢看到。
有兴趣的同学可以听一下尤小右当初为什么要开发 Vue 的 podcast,很有意思,在这里你能很快找到 Vue 的核心并且理解为什么需要它们。