资讯专栏INFORMATION COLUMN

Vue 1.0.28 源码解析

URLOS / 1293人阅读

摘要:整体概览源码最终是向外部抛出一个的构造函数,见源码在源码最开始,通过方法见源码向构造函数添加全局方法,如等,主要初始化一些全局使用的方法变量和配置实例化当使用时,最基本使用方式如下此时,会调用构造函数实例化一个对象,而在构造函数中只有这句代

整体概览

Vue源码最终是向外部抛出一个Vue的构造函数,见源码:

function Vue (options) {
  this._init(options)
}

在源码最开始,通过installGlobalAPI方法(见源码)向Vue构造函数添加全局方法,如Vue.extend、Vue.nextTick、Vue.delete等,主要初始化Vue一些全局使用的方法、变量和配置;

export default function (Vue){
    Vue.options = {
          .....
    }
    Vue.extend = function (extendOptions){
           ......
    }
    Vue.use = function (plugin){
           ......
    }
    Vue.mixin = function (mixin){
           ......
    }
    Vue.extend = function (extendOptions){
           ......
    }
}
实例化Vue

当使用vue时,最基本使用方式如下:

var app = new Vue({
  el: "#app",
  data: {
    message: "Hello Vue!"
  }
})

此时,会调用构造函数实例化一个vue对象,而在构造函数中只有这句代码this.init(options);而在init中(源码),主要进行一些变量的初始化、option重组、各种状态、事件初始化;如下:

Vue.prototype._init = function (options) {
    options = options || {}
    this.$el = null
    this.$parent = options.parent
    this.$root = this.$parent
      ? this.$parent.$root
      : this
    this.$children = []
    this.$refs = {}       // child vm references
    this.$els = {}        // element references
    this._watchers = []   // all watchers as an array
    this._directives = [] // all directives

    ...... // 更多见源码

    options = this.$options = mergeOptions(
      this.constructor.options,
      options,
      this
    )

    // set ref
    this._updateRef()

    // initialize data as empty object.
    // it will be filled up in _initData().
    this._data = {}

    // call init hook
    this._callHook("init")

    // initialize data observation and scope inheritance.
    this._initState()

    // setup event system and option events.
    this._initEvents()

    // call created hook
    this._callHook("created")

    // if `el` option is passed, start compilation.
    if (options.el) {
      this.$mount(options.el)
    }
}

在其中通过mergeOptions方法,将全局this.constructor.options与传入的options及实例化的对象进行合并;而this.constructor.options则是上面初始化vue时进行配置的,其中主要包括一些全局使用的指令、过滤器,如经常使用的"v-if"、"v-for"、"v-show"、"currency":

this.constructor.options = {
        directives: {
          bind: {}, // v-bind
          cloak: {}, // v-cloak
          el: {}, // v-el
          for: {}, // v-for
          html: {}, // v-html
          if: {}, // v-if
          for: {}, // v-for
          text: {}, // v-text
          model: {}, // v-model
          on: {}, // v-on
          show: {} // v-show
        },
        elementDirectives: {
          partial: {}, //  api: https://v1.vuejs.org/api/#partial
          slot: {} // 
        },
        filters: {  // api: https://v1.vuejs.org/api/#Filters
          capitalize: function() {}, // {{ msg | capitalize }}  ‘abc’ => ‘Abc’
          currency: funnction() {},
          debounce: function() {},
          filterBy: function() {},
          json: function() {},
          limitBy: function() {},
          lowercase: function() {},
          orderBy: function() {},
          pluralize: function() {},
          uppercase: function() {}
        }
}

然后,会触发初始化一些状态、事件、触发init、create钩子;然后随后,会触发this.$mount(options.el);进行实例挂载,将dom添加到页面;而this.$mount()方法则包含了绝大部分页面渲染的代码量,包括模板的嵌入、编译、link、指令和watcher的生成、批处理的执行等等,后续会详细进行说明;

_compile函数之transclude

在上面说了下,在Vue.prototype.$mount完成了大部分工作,而在$mount方法里面,最主要的工作量由this._compile(el)承担;其主要包括transclude(嵌入)、compileRoot(根节点编译)、compile(页面其他的编译);而在这儿主要说明transclude方法;

通过对transclude进行网络翻译结果是"嵌入";其主要目的是将页面中自定义的节点转化为真实的html节点;如一个组件其实际dom为

hello {{message}}

,源码; 当我们使用时
; 会通过transclude将其转化为

hello {{message}}

,见源码注释;

那transclude具体干了什么呢,我们先看它的源码:

export function transclude (el, options) {
  // extract container attributes to pass them down
  // to compiler, because they need to be compiled in
  // parent scope. we are mutating the options object here
  // assuming the same object will be used for compile
  // right after this.
  if (options) {
    // 把el(虚拟节点,如)元素上的所有attributes抽取出来存放在了选项对象的_containerAttrs属性上
    // 使用el.attributes 方法获取el上面,并使用toArray方法,将类数组转换为真实数组
    options._containerAttrs = extractAttrs(el)
  }
  // for template tags, what we want is its content as
  // a documentFragment (for fragment instances)
  // 判断是否为 template 标签
  if (isTemplate(el)) {
    // 得到一段存放在documentFragment里的真实dom
    el = parseTemplate(el)
  }
  if (options) {
    if (options._asComponent && !options.template) {
      options.template = ""
    }
    if (options.template) {
      // 将el的内容(子元素和文本节点)抽取出来
      options._content = extractContent(el)
      // 使用options.template 将虚拟节点转化为真实html,  => 

hello {{ msg }}

// 但不包括未绑定数据, 则上面转化为 =>

hello

el = transcludeTemplate(el, options) } } // isFragment: node is a DocumentFragment // 使用nodeType 为 11 进行判断是非为文档片段 if (isFragment(el)) { // anchors for fragment instance // passing in `persist: true` to avoid them being // discarded by IE during template cloning prepend(createAnchor("v-start", true), el) el.appendChild(createAnchor("v-end", true)) } return el }

首先先看如下代码:

if (options) {
    // 把el(虚拟节点,如)元素上的所有attributes抽取出来存放在了选项对象的_containerAttrs属性上
    // 使用el.attributes 方法获取el上面,并使用toArray方法,将类数组转换为真实数组
    options._containerAttrs = extractAttrs(el)
  }

而extractAttrs方法如下,其主要根据元素nodeType去判断是否为元素节点,如果为元素节点,且元素有相关属性,则将属性值取出之后,再转为属性数组;最后将属性数组放到options._containerAttrs中,为什么要这么做呢?因为现在的el可能不是真实的元素,而是诸如,在后面编译过程,需要将其替换为真实的html节点,所以,它上面的属性值都会先取出来预存起来,后面合并到真实html根节点的属性上面;

function extractAttrs (el) {
  // 只查找元素节点及有属性
  if (el.nodeType === 1 && el.hasAttributes()) {
    // attributes 属性返回指定节点的属性集合,即 NamedNodeMap, 类数组
    return toArray(el.attributes)
  }
}

下一句,根据元素nodeName是否为“template”去判断是否为元素;如果是,则走parseTemplate(el)方法,并覆盖当前el对象

if (isTemplate(el)) {
    // 得到一段存放在documentFragment里的真实dom
    el = parseTemplate(el)
  }

function isTemplate (el) {
  return el.tagName &&
    el.tagName.toLowerCase() === "template"
}

parseTemplate则主要是将传入内容生成一段存放在documentFragment里的真实dom;进入函数,首先判断传入是否已经是一个文档片段,如果已经是,则直接返回;否则,判断传入是否为字符串,如果为字符串, 先判断是否是"#test"这种选择器类型,如果是,通过document.getElementById方法取出元素,如果文档中有此元素,将通过nodeToFragment方式,将其放入一个新的节点片段中并赋给frag,最后返回到外面;如果不是选择器类型字符串,则使用stringToFragment将其生成一个新的节点片段,并返回;如果传入非字符串而是节点(不管是什么节点,可以是元素节点、文本节点、甚至Comment节点等);则直接通过nodeToFragment生成节点片段并返回;

export function parseTemplate (template, shouldClone, raw) {
  var node, frag

  // if the template is already a document fragment,
  // do nothing
  // 是否为文档片段, nodetype是否为11
  // https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment
 // 判断传入是否已经是一个文档片段,如果已经是,则直接返回
  if (isFragment(template)) {
    trimNode(template)
    return shouldClone
      ? cloneNode(template)
      : template
  }
  // 判断传入是否为字符串
  if (typeof template === "string") {
    // id selector
    if (!raw && template.charAt(0) === "#") {
      // id selector can be cached too
      frag = idSelectorCache.get(template)
      if (!frag) {
        node = document.getElementById(template.slice(1))
        if (node) {
          frag = nodeToFragment(node)
          // save selector to cache
          idSelectorCache.put(template, frag)
        }
      }
    } else {
      // normal string template
      frag = stringToFragment(template, raw)
    }
  } else if (template.nodeType) {
    // a direct node
    frag = nodeToFragment(template)
  }

  return frag && shouldClone
    ? cloneNode(frag)
    : frag
}

从上面可见,在parseTemplate里面最重要的是nodeToFragment和stringToFragment;那么,它们又是如何将传入内容转化为新的文档片段呢?首先看nodeToFragment:

function nodeToFragment (node) {
  // if its a template tag and the browser supports it,
  // its content is already a document fragment. However, iOS Safari has
  // bug when using directly cloned template content with touch
  // events and can cause crashes when the nodes are removed from DOM, so we
  // have to treat template elements as string templates. (#2805)
  /* istanbul ignore if */
  // 是template元素或者documentFragment,使用stringToFragment转化并保存节点内容
  if (isRealTemplate(node)) {
    return stringToFragment(node.innerHTML)
  }
  // script template
  if (node.tagName === "SCRIPT") {
    return stringToFragment(node.textContent)
  }
  // normal node, clone it to avoid mutating the original
  var clonedNode = cloneNode(node)
  var frag = document.createDocumentFragment()
  var child
  /* eslint-disable no-cond-assign */
  while (child = clonedNode.firstChild) {
  /* eslint-enable no-cond-assign */
    frag.appendChild(child)
  }
  trimNode(frag)
  return frag
}

其实看源码,很容易理解,首先判断传入内容是否为template元素或者documentFragment或者script标签,如果是,都直接走stringToFragment;后面就是先使用document.createDocumentFragment创建一个文档片段,然后将节点进行循环appendChild到创建的文档片段中,并返回新的片段;
那么,stringToFragment呢?这个就相对复杂一点了,如下:

function stringToFragment (templateString, raw) {
  // try a cache hit first
  var cacheKey = raw
    ? templateString
    : templateString.trim() //trim() 方法会从一个字符串的两端删除空白字符
  var hit = templateCache.get(cacheKey)
  if (hit) {
    return hit
  }
  // 创建一个文档片段
  var frag = document.createDocumentFragment()
  // tagRE: /<([w:-]+)/
  // 匹配标签
  // "".match(/<([w:-]+)/) => [""]
  var tagMatch = templateString.match(tagRE)
  // entityRE: /&#?w+?;/
  var entityMatch = entityRE.test(templateString)
  // commentRE: /
  

它对应的descriptor就是:

descriptor = {
    arg: undefined,
    attr: "v-demo",
    def: {
        bind: function() {}, // 上面定义的bind
        update: function() {} // 上面定义的update
    },
    expression:"demo",
    filters: undefined,
    modifiers: {},
    name: "demo"
}

接着上面的,使用extend(this, def)就将def中定义的方法或属性就复制到实例化指令对象上面;好供后面使用;

// initial bind
  if (this.bind) {
    this.bind()
  }

这就是执行上面刚刚保存的bind方法;当执行此方法时,上面就会执行

this.el.setAttribute("style", "color: green");

将字体颜色改为绿色;

// 下面这些判断是因为许多指令比如slot component之类的并不是响应式的,
  // 他们只需要在bind里处理好dom的分发和编译/link即可然后他们的使命就结束了,生成watcher和收集依赖等步骤根本没有
  // 所以根本不用执行下面的处理
if (this.literal) {

} else if (
    (this.expression || this.modifiers) &&
    (this.update || this.twoWay) &&
    !this._checkStatement()
) {

var watcher = this._watcher = new Watcher(
      this.vm,
      this.expression,
      this._update, // callback
      {
        filters: this.filters,
        twoWay: this.twoWay,
        deep: this.deep,
        preProcess: preProcess,
        postProcess: postProcess,
        scope: this._scope
      }
    )
}

而这儿就是对需要添加双向绑定的指令添加watcher;对应watcher后面再进行详细说明; 可以从上看出,传入了this._update方法,其实也就是当数据变化时,就会执行this._update方法,而:

var dir = this
if (this.update) {
      // 处理一下原本的update函数,加入lock判断
      this._update = function (val, oldVal) {
        if (!dir._locked) {
          dir.update(val, oldVal)
        }
      }
} else {
      this._update = function() {}
}

其实也就是执行上面的descriptor.def.update方法,所以当值变化时,会触发我们自定义指令时定义的update方法,而发生颜色变化;

这是指令最主要的代码部分;其他的如下:

// 获取指令的参数, 对于一些指令, 指令的元素上可能存在其他的attr来作为指令运行的参数
  // 比如v-for指令,那么元素上的attr: track-by="..." 就是参数
  // 比如组件指令,那么元素上可能写了transition-mode="out-in", 诸如此类
this._setupParams();

// 当一个指令需要销毁时,对其进行销毁处理;此时,如果定义了unbind方法,也会在此刻调用
this._teardown();
而对于每个指令的处理原理,可以看其对应源码;如v-show源码:

// src/directives/public/show.js

import { getAttr, inDoc } from "../../util/index"
import { applyTransition } from "../../transition/index"

export default {

  bind () {
    // check else block
    var next = this.el.nextElementSibling
    if (next && getAttr(next, "v-else") !== null) {
      this.elseEl = next
    }
  },

  update (value) {
    this.apply(this.el, value)
    if (this.elseEl) {
      this.apply(this.elseEl, !value)
    }
  },

  apply (el, value) {
    if (inDoc(el)) {
      applyTransition(el, value ? 1 : -1, toggle, this.vm)
    } else {
      toggle()
    }
    function toggle () {
      el.style.display = value ? "" : "none"
    }
  }
}

可以从上面看出在初始化页面绑定时,主要获取后面兄弟元素是否使用v-else; 如果使用,将元素保存到this.elseEl中,而当值变化执行update时,主要执行了this.apply;而最终只是执行了下面代码:

el.style.display = value ? "" : "none"

从而达到隐藏或者展示元素的效果;

未完待续,后续会持续完善......

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

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

相关文章

  • JavaScript 进阶之深入理解数据双向绑定

    摘要:当我们的视图和数据任何一方发生变化的时候,我们希望能够通知对方也更新,这就是所谓的数据双向绑定。返回值返回传入函数的对象,即第一个参数该方法重点是描述,对象里目前存在的属性描述符有两种主要形式数据描述符和存取描述符。 前言 谈起当前前端最热门的 js 框架,必少不了 Vue、React、Angular,对于大多数人来说,我们更多的是在使用框架,对于框架解决痛点背后使用的基本原理往往关注...

    sarva 评论0 收藏0
  • vue-loader 源码解析系列之 selector

    摘要:当前正在处理的节点,以及该节点的和等信息。源码解析之一整体分析源码解析之三写作中源码解析之四写作中作者博客作者作者微博 笔者系 vue-loader 贡献者之一(#16) 前言 vue-loader 源码解析系列之一,阅读该文章之前,请大家首先参考大纲 vue-loader 源码解析系列之 整体分析 selector 做了什么 const path = require(path) co...

    miqt 评论0 收藏0
  • Vue源码解析:AST语法树转render函数

    摘要:源码解析这边解析的是从树转换成函数部分的源码,由于第一次提交的源码这部分不全,故做了部分更新,代码全在文件夹中。入口整个语法树转函数的起点是文件中的函数明显看到,函数传入参数为语法树,内部调用函数开始解析根节点容器节点。 通过对 Vue2.0 源码阅读,想写一写自己的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写: 模版字符串转AST语法树 AST语法树转r...

    trilever 评论0 收藏0
  • Vue源码解析:AST语法树转render函数

    摘要:源码解析这边解析的是从树转换成函数部分的源码,由于第一次提交的源码这部分不全,故做了部分更新,代码全在文件夹中。入口整个语法树转函数的起点是文件中的函数明显看到,函数传入参数为语法树,内部调用函数开始解析根节点容器节点。 通过对 Vue2.0 源码阅读,想写一写自己的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写: 模版字符串转AST语法树 AST语法树转r...

    Karuru 评论0 收藏0

发表评论

0条评论

URLOS

|高级讲师

TA的文章

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