Vue.js源码(1):Hello World的背后
下面的代码会在页面上输出Hello World,但是在这个new Vue()到页面渲染之间,到底发生了什么。这篇文章希望通过最简单的例子,去了解Vue源码过程。这里分析的源码版本是Vue.version = 1.0.20
div id="mountNode" {{message}} /div
var vm = new Vue({ el: #mountNode, data: function () { return { message: Hello World }; }
这篇文章将要解决几个问题:
new Vue()的过程中,内部到底有哪些步骤 如何收集依赖 如何计算表达式 如何表达式的值如何反应在DOM上的简单来说过程是这样的:
observe: 把{message: Hello World}变成是reactive的 compile: compileTextNode "{{message}}",解析出指令(directive = v-text)和表达式(expression = message),创建fragment(new TextNode)准备替换 link:实例化directive,将创建的fragment和directive链接起来,将fragment替换在DOM上 bind: 通过directive对应的watcher获取依赖(message)的值("Hello World"),v-text去update值到fragment上详细过程,接着往下看。
构造函数
文件路径:src/instance/vue.js
初始化
这里只拿对例子理解最关键的步骤分析。文件路径:src/instance/internal/init.js
Vue.prototype._init = function (options) { ... // merge options. options = this.$options = mergeOptions( this.constructor.options, options, this ) ... // initialize data observation and scope inheritance. this._initState() ... // if `el` option is passed, start compilation. if (options.el) { this.$mount(options.el) }
merge options
mergeOptions()定义在src/util/options.js文件中,这里主要定义options中各种属性的合并(merge),例如:props, methods, computed, watch等。另外,这里还定义了每种属性merge的默认算法(strategy),这些strategy都可以配置的,参考Custom Option Merge Strategy
在本文的例子中,主要是data选项的merge,在merge之后,放到$options.data中,基本相当于下面这样:
vm.$options.data = function mergedInstanceDataFn () { var parentVal = undefined // 这里就是在我们定义的options中的data var childVal = function () { return { message: Hello World } } // data function绑定vm实例后执行,执行结果: {message: Hello World} var instanceData = childVal.call(vm) // 对象之间的merge,类似$.extend,结果肯定就是:{message: Hello World} return mergeData(instanceData, parentVal)
init data
_initData()发生在_initState()中,主要做了两件事:
代理data中的属性 observe data文件路径:src/instance/internal/state.js
Vue.prototype._initState = function () { this._initProps() this._initMeta() this._initMethods() this._initData() // 这里 this._initComputed() }
属性代理(proxy)
把data的结果赋值给内部属性:文件路径:src/instance/internal/state.js
var dataFn = this.$options.data // 上面我们得到的mergedInstanceDataFn函数 var data = this._data = dataFn ? dataFn() : {}
代理(proxy)data中的属性到_data,使得vm.message === vm._data.message:
文件路径:src/instance/internal/state.js
* Proxy a property, so that * vm.prop === vm._data.prop */ Vue.prototype._proxy = function (key) { if (!isReserved(key)) { var self = this Object.defineProperty(self, key, { configurable: true, enumerable: true, get: function proxyGetter () { return self._data[key] }, set: function proxySetter (val) { self._data[key] = val } }) }
observe
这里是我们的第一个重点,observe过程。在_initData()最后,调用了observe(data, this)对数据进行observe。在hello world例子里,observe()函数主要是针对{message: Hello World}创建了Observer对象。
文件路径:src/observer/index.js
var ob = new Observer(value) // value = data = {message:Hello World}
在observe()函数中还做了些能否observe的条件判断,这些条件有:
没有被observe过(observe过的对象都会被添加__ob__属性) 只能是plain object(toString.call(ob) === "[object Object]")或者数组 不能是Vue实例(obj._isVue !== true) object是extensible的(Object.isExtensible(obj) === true)Observer
官网的Reactivity in Depth上有这么句话:
When you pass a plain JavaScript object to a Vue instance as its data option, Vue.js will walk through all of its properties and convert them to getter/setters
The getter/setters are invisible to the user, but under the hood they enable Vue.js to perform dependency-tracking and change-notification when properties are accessed or modified
Observer就是干这个事情的,使data变成“发布者”,watcher是订阅者,订阅data的变化。
在例子中,创建observer的过程是:
new Observer({message: Hello World}) 实例化一个Dep对象,用来收集依赖 walk(Observer.prototype.walk())数据的每一个属性,这里只有message 将属性变成reactive的(Observer.protoype.convert())convert()里调用了defineReactive(),给data的message属性添加reactiveGetter和reactiveSetter
文件路径:src/observer/index.js
export function defineReactive (obj, key, value) { ... Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { ... if (Dep.target) { dep.depend() // 这里是收集依赖 ... } return value }, set: function reactiveSetter (newVal) { ... if (setter) { setter.call(obj, newVal) } else { val = newVal } ... dep.notify() // 这里是notify观察这个数据的依赖(watcher) } })
关于依赖收集和notify,主要是Dep类
文件路径:src/observer/dep.js
export default function Dep () { this.id = uid++ this.subs = []
这里的subs是保存着订阅者(即watcher)的数组,当被观察数据发生变化时,即被调用setter,那么dep.notify()就循环这里的订阅者,分别调用他们的update方法。
但是在getter收集依赖的代码里,并没有看到watcher被添加到subs中,什么时候添加进去的呢?这个问题在讲到Watcher的时候再回答。
mount node
按照生命周期图上,observe data和一些init之后,就是$mount了,最主要的就是_compile。
文件路径:src/instance/api/lifecycle.js
Vue.prototype.$mount = function (el) { ... this._compile(el) ... }
_compile里分两步:compile和link
compile
compile过程是分析给定元素(el)或者模版(template),提取指令(directive)和创建对应离线的DOM元素(document fragment)。
文件路径:src/instance/internal/lifecycle.js
Vue.prototype._compile = function (el) { ... var rootLinker = compileRoot(el, options, contextOptions) ... var rootUnlinkFn = rootLinker(this, el, this._scope) ... var contentUnlinkFn = compile(el, options)(this, el) ...
例子中compile #mountNode元素,大致过程如下:
compileRoot:由于root node( div id="mountNode" /div )本身没有任何指令,所以这里compile不出什么东西 compileChildNode:mountNode的子node,即内容为"{{message}}"的TextNode compileTextNode:3.1 parseText:其实就是tokenization(标记化:从字符串中提取符号,语句等有意义的元素),得到的结果是tokens
3.2 processTextToken:从tokens中分析出指令类型,表达式和过滤器,并创建新的空的TextNode
3.3 创建fragment,将新的TextNode append进去
parseText的时候,通过正则表达式(/\{\{\{(.+?)\}\}\}|\{\{(.+?)\}\}/g)匹配字符串"{{message}}",得出的token包含这些信息:“这是个tag,而且是文本(text)而非HTML的tag,不是一次性的插值(one-time interpolation),tag的内容是"message"”。这里用来做匹配的正则表达式是会根据delimiters和unsafeDelimiters的配置动态生成的。
processTextToken之后,其实就得到了创建指令需要的所有信息:指令类型v-text,表达式"message",过滤器无,并且该指令负责跟进的DOM是新创建的TextNode。接下来就是实例化指令了。
link
每个compile函数之后都会返回一个link function(linkFn)。linkFn就是去实例化指令,将指令和新建的元素link在一起,然后将元素替换到DOM tree中去。每个linkFn函数都会返回一个unlink function(unlinkFn)。unlinkFn是在vm销毁的时候用的,这里不介绍。
实例化directive:new Directive(description, vm, el)
description是compile结果token中保存的信息,内容如下:
description = { name: text, // text指令 expression: message, filters: undefined, def: vTextDefinition
def属性上的是text指令的定义(definition),和Custome Directive一样,text指令也有bind和update方法,其定义如下:
文件路径:src/directives/public/text.js
export default { bind () { this.attr = this.el.nodeType === 3 ? data : textContent }, update (value) { this.el[this.attr] = _toString(value)
new Directive()构造函数里面只是一些内部属性的赋值,真正的绑定过程还需要调用Directive.prototype._bind,它是在Vue实例方法_bindDir()中被调用的。
在_bind里面,会创建watcher,并第一次通过watcher去获得表达式"message"的计算值,更新到之前新建的TextNode中去,完成在页面上渲染"Hello World"。
watcher
For every directive / data binding in the template, there will be a corresponding watcher object, which records any properties “touched” during its evaluation as dependencies. Later on when a dependency’s setter is called, it triggers the watcher to re-evaluate, and in turn causes its associated directive to perform DOM updates.
每个与数据绑定的directive都有一个watcher,帮它监听表达式的值,如果发生变化,则通知它update自己负责的DOM。一直说的dependency collection就在这里发生。
Directive.prototype._bind()里面,会new Watcher(expression, update),把表达式和directive的update方法传进去。
Watcher会去parseExpression:
文件路径:src/parsers/expression.js
export function parseExpression (exp, needSet) { exp = exp.trim() // try cache var hit = expressionCache.get(exp) if (hit) { if (needSet !hit.set) { hit.set = compileSetter(hit.exp) } return hit var res = { exp: exp } res.get = isSimplePath(exp) exp.indexOf([) 0 // optimized super simple getter ? makeGetterFn(scope. + exp) // dynamic getter : compileGetter(exp) if (needSet) { res.set = compileSetter(exp) expressionCache.put(exp, res) return res
这里的expression是"message",单一变量,被认为是简单的数据访问路径(simplePath)。simplePath的值如何计算,怎么通过"message"字符串获得data.message的值呢?
获取字符串对应的变量的值,除了用eval,还可以用Function。上面的makeGetterFn(scope. + exp)返回:
var getter = new Function(scope, return + body + ;) // new Function(scope, return scope.message;)
Watch.prototype.get()获取表达式值的时候,
var scope = this.vm getter.call(scope, scope) // 即执行vm.message
由于initState时对数据进行了代理(proxy),这里的vm.message即为vm._data.message,即是data选项中定义的"Hello World"。
值拿到了,那什么时候将message设为依赖的呢?这就要结合前面observe data里说到的reactiveGetter了。
文件路径:src/watcher.js
Watcher.prototype.get = function () { this.beforeGet() // - Dep.target = this var scope = this.scope || this.vm ... var value value = this.getter.call(scope, scope) ... this.afterGet() // - Dep.target = null return value
watcher获取表达式的值分三步:
beforeGet:设置Dep.target = this 调用表达式的getter,读取(getter)vm.message的值,进入了message的reactiveGetter,由于Dep.target有值,因此执行了dep.depend()将target,即当前watcher,收入dep.subs数组里 afterGet:设置Dep.target = null这里值得注意的是Dep.target,由于JS的单线程特性,同一时刻只能有一个watcher去get数据的值,所以target在全局下只需要有一个就可以了。
文件路径:src/observer/dep.js
// the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. Dep.target = null
就这样,指令通过watcher,去touch了表达式中涉及到的数据,同时被该数据(reactive data)保存为其变化的订阅者(subscriber),数据变化时,通过dep.notify() - watcher.update() - directive.update() - textDirective.update(),完成DOM的更新。
到这里,“Hello World”怎么渲染到页面上的过程基本就结束了。这里针对最简单的使用,挑选了最核心的步骤进行分析,更多内部细节,后面慢慢分享。
作者:外籍杰克
来源:51CTO
深入解析 Vue 的热更新原理,尤大是如何巧用源码中的细节? 大家都用过 Vue-CLI 创建 vue 应用,在开发的时候我们修改了 vue 文件,保存了文件,浏览器上就自动更新出我们写的组件内容,非常的顺滑流畅,大大提高了开发效率。想知道这背后是怎么实现的吗,其实代码并不复杂。
实现最精简的响应式系统来学习Vue的data、computed、watch源码 记得初学Vue源码的时候,在defineReactive、Observer、Dep、Watcher等等内部设计源码之间跳来跳去,发现再也绕不出来了。Vue发展了很久,很多fix和feature的增加让内部源码越来越庞大,太多的边界情况和优化设计掩盖了原本精简的代码设计,让新手阅读源码变得越来越困难,但是面试的时候,Vue的响应式原理几乎成了Vue技术栈的公司面试中高级前端必问的点之一。
Vue(v2.6.11)万行源码生啃,就硬刚!(下) 众所周知,以下代码就是 vue 的一种直接上手方式。通过 cdn 可以在线打开 vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。
Vue(v2.6.11)万行源码生啃,就硬刚!(中) 众所周知,以下代码就是 vue 的一种直接上手方式。通过 cdn 可以在线打开 vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。
Vue(v2.6.11)万行源码生啃,就硬刚!(上) 众所周知,以下代码就是 vue 的一种直接上手方式。通过 cdn 可以在线打开 vue.js。一个文件,一万行源码,是万千开发者赖以生存的利器,它究竟做了什么?让人品味。
相关文章
- vue - 减少打包后的体积
- 【Vue】【子组件】调用【父组件】参数,props属性值【动态】调用的两种方法(图文+完整代码)
- Vue - 实现微信扫码登录功能(项目植入微信扫码登录功能)超详细完整流程详解及详细代码及注释,附带完整功能源码、常见问题解决方案
- Vue - 搜索关键字标红高亮(用户输入关键词搜索后,在搜索结果的列表标题上匹配并标红加粗)怎么使内容文本标红高亮的最详细教程,Nuxt.js uni-app 也适用,搜索功能及搜索结果关键字高亮源码
- Vue - 下载后端接口返回的文件流(js-file-download)
- Vue Nuxt.js - 根据后端返回的唯一 ID / code,生成 “唯一“ 的推荐码、邀请码、订单号、加密路由、一串英文+数字长字符等 (支持反序列化原 ID 解码,逆向得出 ID 二者互转)
- Vue - 纯前端导出 Table 表格本页数据(Excel)
- JavaScript - math.js 数学库,实现 math.add() “数字累加“ 计算总和的操作(例如:循环 N 个整数或小数,然后再循环内 “+=“ 叠加算数)适用于Vue等全部前端项目
- Node.js安装使用-VueCLI安装使用-工程化的Vue.js开发
- (7)打鸡儿教你Vue.js
- vue.js精讲02
- vue中使用Base64和md5和rsa
- Vue生命周期钩子---2
- vue中的main.js打开直接报错问题解决
- Vue+ElementUI 导航组件
- vue使用自定义指令监听元素宽、高变化
- vue中路由跳转后刷新页面
- vue-cli3的vue.config.js文件配置,生成dist文件
- [js高手之路]Vue2.0基于vue-cli+webpack同级组件之间的通信教程
- [js高手之路] vue系列教程 - 绑定设置属性的多种方式(5)
- [js高手之路] vue系列教程 - 实现留言板todolist(3)
- vue.js 使用 fastclick解决移动端click事件300毫秒延迟方法
- vue前台(三)采用申明式导航去点击连接到其他组件(采用事件委派方式)
- idea中启动vue项目
- 从零开始学 Web 之 Vue.js(四)Vue的Ajax请求和跨域
- vue:pdf.js使用细节/隐藏按钮/设置、获取当前页码/记录阅读进度/切换语言(国际化)