Vue.js源码(1):Hello World的背后

简介:


下面的代码会在页面上输出Hello World,但是在这个new Vue()到页面渲染之间,到底发生了什么。这篇文章希望通过最简单的例子,去了解Vue源码过程。这里分析的源码版本是Vue.version = '1.0.20'

 
  1. <div id="mountNode">{{message}}</div>  
 
  1. var vm = new Vue({ 
  2.     el: '#mountNode'
  3.     data: function () { 
  4.         return { 
  5.             message: 'Hello World' 
  6.         }; 
  7.     } 
  8. }); 

这篇文章将要解决几个问题:

  1. new Vue()的过程中,内部到底有哪些步骤
  2. 如何收集依赖
  3. 如何计算表达式
  4. 如何表达式的值如何反应在DOM上的

简单来说过程是这样的:

  1. observe: 把{message: 'Hello World'}变成是reactive的
  2. compile: compileTextNode "{{message}}",解析出指令(directive = v-text)和表达式(expression = message),创建fragment(new TextNode)准备替换
  3. link:实例化directive,将创建的fragment和directive链接起来,将fragment替换在DOM上
  4. bind: 通过directive对应的watcher获取依赖(message)的值("Hello World"),v-text去update值到fragment上

详细过程,接着往下看。

构造函数

文件路径:src/instance/vue.js

 
  1. function Vue (options) { 
  2.   this._init(options) 
  3. }  

初始化

这里只拿对例子理解最关键的步骤分析。文件路径:src/instance/internal/init.js

 
  1. Vue.prototype._init = function (options) { 
  2.     ... 
  3.     // merge options. 
  4.     options = this.$options = mergeOptions( 
  5.       this.constructor.options, 
  6.       options, 
  7.       this 
  8.     ) 
  9.     ... 
  10.     // initialize data observation and scope inheritance. 
  11.     this._initState() 
  12.     ... 
  13.     // if `el` option is passed, start compilation. 
  14.     if (options.el) { 
  15.       this.$mount(options.el) 
  16.     } 

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中,基本相当于下面这样:

 
  1. vm.$options.data = function mergedInstanceDataFn () { 
  2.       var parentVal = undefined 
  3.        
  4.       // 这里就是在我们定义的options中的data 
  5.       var childVal = function () { 
  6.           return { 
  7.               message: 'Hello World' 
  8.           } 
  9.       } 
  10.        
  11.       // data function绑定vm实例后执行,执行结果: {message: 'Hello World'
  12.       var instanceData = childVal.call(vm) 
  13.        
  14.       // 对象之间的merge,类似$.extend,结果肯定就是:{message: 'Hello World'
  15.       return mergeData(instanceData, parentVal) 
  16. }  

init data

_initData()发生在_initState()中,主要做了两件事:

  1. 代理data中的属性
  2. observe data

文件路径:src/instance/internal/state.js

 
  1. Vue.prototype._initState = function () { 
  2.     this._initProps() 
  3.     this._initMeta() 
  4.     this._initMethods() 
  5.     this._initData() // 这里 
  6.     this._initComputed() 
  7.   }  

属性代理(proxy)

把data的结果赋值给内部属性:文件路径:src/instance/internal/state.js

 
  1. var dataFn = this.$options.data // 上面我们得到的mergedInstanceDataFn函数 
  2. var data = this._data = dataFn ? dataFn() : {}  

代理(proxy)data中的属性到_data,使得vm.message === vm._data.message:

文件路径:src/instance/internal/state.js

 
  1. /** 
  2.   * Proxy a property, so that 
  3.   * vm.prop === vm._data.prop 
  4.   */ 
  5. Vue.prototype._proxy = function (key) { 
  6.     if (!isReserved(key)) { 
  7.       var self = this 
  8.       Object.defineProperty(self, key, { 
  9.         configurable: true
  10.         enumerable: true
  11.         get: function proxyGetter () { 
  12.           return self._data[key
  13.         }, 
  14.         setfunction proxySetter (val) { 
  15.           self._data[key] = val 
  16.         } 
  17.       }) 
  18.     } 
  19.   } 

observe

这里是我们的第一个重点,observe过程。在_initData()最后,调用了observe(data, this)对数据进行observe。在hello world例子里,observe()函数主要是针对{message: 'Hello World'}创建了Observer对象。

文件路径:src/observer/index.js

 
  1. var ob = new Observer(value) // value = data = {message:'Hello World'

在observe()函数中还做了些能否observe的条件判断,这些条件有:

  1. 没有被observe过(observe过的对象都会被添加__ob__属性)
  2. 只能是plain object(toString.call(ob) === "[object Object]")或者数组
  3. 不能是Vue实例(obj._isVue !== true)
  4. 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的过程是:

  1. new Observer({message: 'Hello World'}) 
  2. 实例化一个Dep对象,用来收集依赖
  3. walk(Observer.prototype.walk())数据的每一个属性,这里只有message
  4. 将属性变成reactive的(Observer.protoype.convert())

convert()里调用了defineReactive(),给data的message属性添加reactiveGetter和reactiveSetter

文件路径:src/observer/index.js

 
  1. export function defineReactive (obj, key, value) { 
  2.     ... 
  3.     Object.defineProperty(obj, key, { 
  4.     enumerable: true
  5.     configurable: true
  6.     get: function reactiveGetter () { 
  7.       ... 
  8.       if (Dep.target) { 
  9.         dep.depend() // 这里是收集依赖 
  10.         ... 
  11.       } 
  12.       return value 
  13.     }, 
  14.     setfunction reactiveSetter (newVal) { 
  15.       ... 
  16.       if (setter) { 
  17.         setter.call(obj, newVal) 
  18.       } else { 
  19.         val = newVal 
  20.       } 
  21.       ... 
  22.       dep.notify() // 这里是notify观察这个数据的依赖(watcher) 
  23.     } 
  24.   }) 

关于依赖收集和notify,主要是Dep类

文件路径:src/observer/dep.js

 
  1. export default function Dep () { 
  2.   this.id = uid++ 
  3.   this.subs = [] 

这里的subs是保存着订阅者(即watcher)的数组,当被观察数据发生变化时,即被调用setter,那么dep.notify()就循环这里的订阅者,分别调用他们的update方法。

但是在getter收集依赖的代码里,并没有看到watcher被添加到subs中,什么时候添加进去的呢?这个问题在讲到Watcher的时候再回答。

mount node

按照生命周期图上,observe data和一些init之后,就是$mount了,最主要的就是_compile。

文件路径:src/instance/api/lifecycle.js

 
  1. Vue.prototype.$mount = function (el) { 
  2.     ... 
  3.     this._compile(el) 
  4.     ... 
  5.   }  

_compile里分两步:compile和link

compile

compile过程是分析给定元素(el)或者模版(template),提取指令(directive)和创建对应离线的DOM元素(document fragment)。

文件路径:src/instance/internal/lifecycle.js

 
  1. Vue.prototype._compile = function (el) { 
  2.     ... 
  3.     var rootLinker = compileRoot(el, options, contextOptions) 
  4.     ... 
  5.     var rootUnlinkFn = rootLinker(this, el, this._scope) 
  6.     ... 
  7.     var contentUnlinkFn = compile(el, options)(this, el) 
  8.     ... 

例子中compile #mountNode元素,大致过程如下:

  1. compileRoot:由于root node(<div id="mountNode"></div>)本身没有任何指令,所以这里compile不出什么东西
  2. compileChildNode:mountNode的子node,即内容为"{{message}}"的TextNode
  3. 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中保存的信息,内容如下:

 
  1. description = { 
  2.     name'text', // text指令 
  3.     expression: 'message'
  4.     filters: undefined, 
  5.     def: vTextDefinition 

def属性上的是text指令的定义(definition),和Custome Directive一样,text指令也有bind和update方法,其定义如下:

文件路径:src/directives/public/text.js

 
  1. export default { 
  2.  
  3.   bind () { 
  4.     this.attr = this.el.nodeType === 3 
  5.       ? 'data' 
  6.       : 'textContent' 
  7.   }, 
  8.  
  9.   update (value) { 
  10.     this.el[this.attr] = _toString(value) 
  11.   } 

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

 
  1. export function parseExpression (exp, needSet) { 
  2.   exp = exp.trim() 
  3.   // try cache 
  4.   var hit = expressionCache.get(exp) 
  5.   if (hit) { 
  6.     if (needSet && !hit.set) { 
  7.       hit.set = compileSetter(hit.exp) 
  8.     } 
  9.     return hit 
  10.   } 
  11.   var res = { exp: exp } 
  12.   res.get = isSimplePath(exp) && exp.indexOf('[') < 0 
  13.     // optimized super simple getter 
  14.     ? makeGetterFn('scope.' + exp) 
  15.     // dynamic getter 
  16.     : compileGetter(exp) 
  17.   if (needSet) { 
  18.     res.set = compileSetter(exp) 
  19.   } 
  20.   expressionCache.put(exp, res) 
  21.   return res 
  22. }  

这里的expression是"message",单一变量,被认为是简单的数据访问路径(simplePath)。simplePath的值如何计算,怎么通过"message"字符串获得data.message的值呢?

获取字符串对应的变量的值,除了用eval,还可以用Function。上面的makeGetterFn('scope.' + exp)返回:

 
  1. var getter = new Function('scope''return ' + body + ';') // new Function('scope''return scope.message;'

Watch.prototype.get()获取表达式值的时候,

 
  1. var scope = this.vm 
  2. getter.call(scope, scope) // 即执行vm.message  

由于initState时对数据进行了代理(proxy),这里的vm.message即为vm._data.message,即是data选项中定义的"Hello World"。

值拿到了,那什么时候将message设为依赖的呢?这就要结合前面observe data里说到的reactiveGetter了。

文件路径:src/watcher.js

 
  1. Watcher.prototype.get = function () { 
  2.   this.beforeGet()        // -> Dep.target = this 
  3.   var scope = this.scope || this.vm 
  4.   ... 
  5.   var value value = this.getter.call(scope, scope) 
  6.   ... 
  7.   this.afterGet()         // -> Dep.target = null 
  8.   return value 
  9. }  

watcher获取表达式的值分三步:

  1. beforeGet:设置Dep.target = this
  2. 调用表达式的getter,读取(getter)vm.message的值,进入了message的reactiveGetter,由于Dep.target有值,因此执行了dep.depend()将target,即当前watcher,收入dep.subs数组里
  3. afterGet:设置Dep.target = null

这里值得注意的是Dep.target,由于JS的单线程特性,同一时刻只能有一个watcher去get数据的值,所以target在全局下只需要有一个就可以了。

文件路径:src/observer/dep.js

 
  1. // the current target watcher being evaluated. 
  2. // this is globally unique because there could be only one 
  3. // watcher being evaluated at any time
  4. Dep.target = null  

就这样,指令通过watcher,去touch了表达式中涉及到的数据,同时被该数据(reactive data)保存为其变化的订阅者(subscriber),数据变化时,通过dep.notify() -> watcher.update() -> directive.update() -> textDirective.update(),完成DOM的更新。

到这里,“Hello World”怎么渲染到页面上的过程基本就结束了。这里针对最简单的使用,挑选了最核心的步骤进行分析,更多内部细节,后面慢慢分享。


作者:外籍杰克

来源:51CTO

相关文章
|
15天前
|
JavaScript 前端开发 容器
AJAX载入外部JS文件到页面并让其执行的方法(附源码)
AJAX载入外部JS文件到页面并让其执行的方法(附源码)
17 0
|
1月前
|
JavaScript
当当网上书店购物车——JS源码
当当网上书店购物车——JS源码
14 0
|
1月前
|
JavaScript 容器
初识Vue 输出Hello World 及注意事项
初识Vue 输出Hello World 及注意事项
|
2月前
|
JavaScript 前端开发 开发者
像素鸟html与js源码(4节课勉强做完)
像素鸟html与js源码(4节课勉强做完)
25 0
|
3月前
|
移动开发 JavaScript 前端开发
分享111个JavaScript源码,总有一款适合您
分享111个JavaScript源码,总有一款适合您
44 1
|
3月前
|
移动开发 JavaScript 前端开发
分享88个JavaScript源码,总有一款适合您
分享88个JavaScript源码,总有一款适合您
28 0
|
3月前
|
JavaScript 前端开发 编译器
看完这篇文章,不再害怕Vue3的源码(一)
看完这篇文章,不再害怕Vue3的源码
|
3月前
|
人工智能 小程序 JavaScript
Vue智慧校园云平台教育小程序源码
智慧校园是一种新型的教育模式,它贯穿整个教育过程,通过信息化技术,将学生、教师、教育资源、管理、服务等多个方面进行全面协调和优化,实现教育信息化、数字化、智能化,为学生创造更好的学习环境和教师更便捷的工作条件。
35 0
|
3月前
|
传感器 人工智能 监控
Springcloud+Vue智慧工地管理云平台源码 AI智能识别
“智慧工地管理平台”以现场实际施工及管理经验为依托,针对工地现场痛点,能在工地落地实施的模块化、一体化综合管理平台。为建筑公司、地产公司、监管单位租赁企业、设备生产厂提供了完整的数据接入和管理服务。
61 2
|
3月前
|
JavaScript 前端开发 算法
用vue想要拿20k,面试题要这样回答(源码版)
用vue想要拿20k,面试题要这样回答(源码版)