资讯专栏INFORMATION COLUMN

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

jlanglang / 1409人阅读

摘要:构造函数文件路径初始化这里只拿对例子理解最关键的步骤分析。在最后,调用了对数据进行。每个函数之后都会返回一个。就是去实例化指令,将指令和新建的元素在一起,然后将元素替换到中去。

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

    
{{message}}
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

function Vue (options) {
  this._init(options)
}
初始化

这里只拿对例子理解最关键的步骤分析。
文件路径: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(

)本身没有任何指令,所以这里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”怎么渲染到页面上的过程基本就结束了。这里针对最简单的使用,挑选了最核心的步骤进行分析,更多内部细节,后面慢慢分享。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90930.html

相关文章

  • Vue.js源码(2):初探List Rendering

    摘要:最后举两个例子,回顾上面的内容例一改变的是数组元素中属性,由于创建的的指令,因此这里直接由指令更新对应元素的内容。 下面例子来自官网,虽然看上去就比Hello World多了一个v-for,但是内部多了好多的处理过程。但是这就是框架,只给你留下最美妙的东西,让生活变得简单。 {{ todo.text }} ...

    shiyang6017 评论0 收藏0
  • Vue.js起手式+Vue小作品实战

    摘要:本文是小羊根据文档进行解读的第一篇文章,主要内容涵盖的基础部分的知识的,文章顺序基本按照官方文档的顺序,每个知识点现附上代码,然后根据代码给予个人的一些理解,最后还放上在线编辑的代码以供练习和测试之用在最后,我参考上的一篇技博,对进行初入的 本文是小羊根据Vue.js文档进行解读的第一篇文章,主要内容涵盖Vue.js的基础部分的知识的,文章顺序基本按照官方文档的顺序,每个知识点现附上代...

    CompileYouth 评论0 收藏0
  • Vue.js起手式+Vue小作品实战

    摘要:本文是小羊根据文档进行解读的第一篇文章,主要内容涵盖的基础部分的知识的,文章顺序基本按照官方文档的顺序,每个知识点现附上代码,然后根据代码给予个人的一些理解,最后还放上在线编辑的代码以供练习和测试之用在最后,我参考上的一篇技博,对进行初入的 本文是小羊根据Vue.js文档进行解读的第一篇文章,主要内容涵盖Vue.js的基础部分的知识的,文章顺序基本按照官方文档的顺序,每个知识点现附上代...

    付伦 评论0 收藏0
  • 【翻译】Next.js背后哲学和设计

    摘要:无数的模板语言和框架应运而生但是技术始终被分割为前端和后端。这意味着一个页面可以有很多的这并不会对其余的页面有任何影响。提前绑定和编译预测是一个非常有效的部署方式。最后,这是我们对于这个特定问题的贡献。 Next.js 原文地址 Naoyuki Kanezawa (@nkzawa), Guillermo Rauch (@rauchg) 和 Tony Kovanen (@tonykova...

    plokmju88 评论0 收藏0
  • 你所不知道HelloWorld背后原理

    摘要:今日最佳对于程序员而言,所谓的二八定律指的是花百分之八十的时间去学习日常研发中不常见的那百分之二十的原理。 【今日最佳】对于程序员而言,所谓的二八定律指的是 花百分之八十的时间去学习日常研发中不常见的那百分之二十的原理。 据说阿里某程序员对书法十分感兴趣,退休后决定在这方面有所建树。于是花重金购买了上等的文房四宝。 一日,饭后突生雅兴,一番磨墨拟纸,并点上了上好的檀香,颇有王羲之风范,...

    lavor 评论0 收藏0

发表评论

0条评论

jlanglang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<