vue 1.0源代码重点难点分析

本文分析vue1.0源代码从入口开始到编译输出到网页显示生效的整个过程,并重点分析一些vue源代码设计原理。

vue初始化根组件的入口代码:

对于没有路由的单页应用来说,入口就是new Vue(options),options就是根组件代码,对于有路由的项目来说,入口就是router.start(app),其中app是根组件。

function Router() {} // router构造函数
var router = new Router(); // new router实例
router.start(app); // 项目真正的入口,app是根组件代码(对象)

Router.prototype.start = function start(App, container, cb) {
  this._appContainer = container; //把app和占位元素container保存在router实例
  var Ctor = this._appConstructor = typeof App === 'function' ? App : Vue.extend(App); // 根组件App继承Vue类(js用构造函数和prototype属性模仿类)
  this.history.start();

HashHistory.prototype.start = function start() {
  self.onChange(path.replace(/^#!?/, '') + query); //当浏览器地址中hash部分变化时触发执行listener(),listener会格式化浏览器地址中hash部分(hash如果没有/则加/前缀),再执行onChange,传入path(url地址的hash部分)

onChange: function onChange(path, state, anchor) { // url地址中hask部分变化触发执行此方法执行进行路由组件切换,类似input元素的输入触发onchange
  _this._match(path, state, anchor);

Router.prototype._match = function _match(path, state, anchor) {
  var _this3 = this; // this是router实例
  var route = new Route(path, this);  // 这是route实例,含路由切换信息from/to,不是router实例。
  var transition = new RouteTransition(this, route, currentRoute); //new transition实例时传入router实例,transition是路由切换from/to相关处理,还有一个transition是路由组件切换时的过渡效果,别搞混了
  if (!this.app) { // 如果是第一次执行,还没有new根组件实例
    _this3.app = new _this3._appConstructor({ //new根组件实例,构造函数是function VueComponent (options) { this._init(options) },new根组件实例保存在router实例中,router.app就是根组件实例,router实例会保存在根组件实例中。


在new Vuecomponent()实例化时要执行Vuecomponent构造函数,Vuecomponent是继承Vue的,就是要执行Vue的_init初始化函数,就开始初始化根组件,编译根组件
template。
vue的钩子函数created()就是指new组件实例之后,而ready()就是指编译组件template之后,先new组件实例,再编译组件template,因为是在创建组件实例之后才
执行构造函数Vue才执行_init()开始初始化编译,编译完成之后插入网页生效,是异步过程,所以执行ready()时可能插入网页生效还没有真正完成,如果ready()有
代码需要在网页找组件元素,比如初始化轮播的代码,可能就需要延迟执行等插入网页生效完成,否则就会出现无法理解的异常现象。

router实例会保存在根组件实例中,传递给各个子组件,因此vue的各个组件访问router实例非常方便。

Vue.prototype._init就是vue组件入口/初始化函数,这是组件通用的初始化函数,是处理组件的核心函数,因为vue是从根组件开始递归编译所有的子组件,
所有的子组件都要跑一遍这个方法,所以这个方法就是vue的顶层核心方法,在底层有几十几百个方法进行各种处理。

先从_init开始把组件初始化过程从头到尾大概走一遍如下(只列举主要语句):

Vue.prototype._init = function (options) {
  this._initState(); //之后el从id变为网页元素DOM(引用网页元素)
  if (options.el) {
    this.$mount(options.el); //如果根组件写了template,el就是根组件占位元素,否则el就是online template元素,从根组件开始递归编译所有的子组件子节点
  }

    Vue.prototype._initState = function () {
      this._initProps();
      this._initMeta();
      this._initMethods();
      this._initData();
      this._initComputed();
    };

      Vue.prototype._initProps = function () {
        // make sure to convert string selectors into element now
        el = options.el = query(el);
      }


Vue.prototype.$mount = function (el) {

  this._compile(el); //从根组件开始递归编译所有的子组件子节点,然后插入网页生效
  return this;
};


Vue.prototype._compile = function (el) {
  var original = el;

  el = transclude(el, options);

  var rootLinker = compileRoot(el, options, contextOptions); // compile产生link函数
  var rootUnlinkFn = rootLinker(this, el, this._scope); // 执行link函数,插入网页生效 

  var contentUnlinkFn = contentLinkFn ? contentLinkFn(this, el) : compile(el, options)(this, el); //编译产生link()再执行link()


  if (options.replace) {
    replace(original, el); //可能需要替换更新
  }

    function compileRoot(el, options, contextOptions) {
      replacerLinkFn = compileDirectives(el.attributes, options); //root元素<div id=  没什么要编译的
      return function rootLinkFn(vm, el, scope) {
    }

      function compileDirectives(attrs, options) {}

    function compile(el, options, partial) {  // 编译组件的template,el是组件占位元素,options是组件代码
      var nodeLinkFn = compileNode(el, options) //编译根节点
      var childLinkFn = compileNodeList(el.childNodes, options) // 递归编译子节点
      return function compositeLinkFn(vm, el, host, scope, frag) {  // 编译返回的通用link函数

          function makeChildLinkFn(linkFns) { // 产生子节点link函数
            return function childLinkFn(vm, nodes, host, scope, frag) { // 子节点link函数返回给var childLinkFn


          function compileNodeList(nodeList, options) {  // 会递归调用自己,层层递归所有子节点
            nodeLinkFn = compileNode(node, options); //编译一个节点
            childLinkFn = compileNodeList(node.childNodes, options) //编译一个节点的子节点,递归编译子节点
            return linkFns.length ? makeChildLinkFn(linkFns) : null; //nodeLinkFn和childLinkFn在linkFns中

                function compileNode(node, options) {  // 编译一个节点
                  var type = node.nodeType;
                  if (type === 1 && !isScript(node)) {
                    return compileElement(node, options);
                  } else if (type === 3 && node.data.trim()) {
                  return compileTextNode(node, options);

                      function compileElement(el, options) { // 到这里要解析处理页面元素表达式中的指令属性包括标签组件,细节本文忽略
                        // check terminal directives (for & if)
                        if (hasAttrs) {
                          linkFn = checkTerminalDirectives(el, attrs, options);
                        }
                        // check element directives
                        if (!linkFn) {
                          linkFn = checkElementDirectives(el, options);
                        }
                        // check component
                        if (!linkFn) {
                          linkFn = checkComponent(el, options);
                        }
                        // normal directives
                        if (!linkFn && hasAttrs) {
                          linkFn = compileDirectives(attrs, options);
                        }
                        return linkFn;

                          function checkTerminalDirectives(el, attrs, options) {  // 编译处理指令
                            def = resolveAsset(options, 'directives', matched[1]); //每种每个属性指令都有一套方法找出来
                            return makeTerminalNodeLinkFn(el, dirName, value, options, termDef, rawName, arg, modifiers);

                              function makeTerminalNodeLinkFn(el, dirName, value, options, def, rawName, arg, modifiers) {
                                var parsed = parseDirective(value); //指令表达式字符串如果没有|,就无需解析,直接返回指令表达式字符串
                                var fn = function terminalNodeLinkFn(vm, el, host, scope, frag) {
                                  if (descriptor.ref) {
                                    defineReactive((scope || vm).$refs, descriptor.ref, null);
                                  }
                                  vm._bindDir(descriptor, el, host, scope, frag);
                                };
                                return fn;

                                    Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {
                                      this._directives.push(new Directive(descriptor, this, node, host, scope, frag));
                                    };

至此解析编译节点的指令表达式结束,然后执行link函数:

function compositeLinkFn(vm, el, host, scope, frag) {
  var dirs = linkAndCapture(function compositeLinkCapturer() {
    if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag);
    if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag);
  }, vm);

function linkAndCapture(linker, vm) {
  linker(); //增加新解析的directive实例存储到vm._directives中
  var dirs = vm._directives.slice(originalDirCount); //取最新增加的directive实例
  sortDirectives(dirs);
  for (var i = 0, l = dirs.length; i < l; i++) {
    dirs[i]._bind(); //执行每个directive实例的_bind方法,比如v-for是一个directive实例,每个循环项又是一个directive实例,组件标签也是一个directive实例
  }
  return dirs;
}

linker()就是要执行编译阶段产生的link函数:
function childLinkFn(vm, nodes, host, scope, frag) {
  nodeLinkFn(vm, node, host, scope, frag);
  childrenLinkFn(vm, childNodes, host, scope, frag); //childrenLinkFn就是childLinkFn,子节点递归调用

  nodeLinkFn()就是编译阶段产生的那个fn:

  var fn = function terminalNodeLinkFn(vm, el, host, scope, frag) {
  if (descriptor.ref) {
    defineReactive((scope || vm).$refs, descriptor.ref, null);  // 如果有v-ref="xxx" 引用组件对象,这也是一种指令属性
  }
  vm._bindDir(descriptor, el, host, scope, frag);  // bind就是初始化方法,bindDir就是初始化指令的方法
};

要执行new Directive(descriptor, this, node, host, scope, frag),然后把directive实例保存到vm._directives中,
new Directive时只是把之前compile/link阶段产生的数据方法保存到directive实例中,并无其它处理,因此只是准备数据,保存到实例,后面再执行实例中的初始化方法初始化指令。


Directive.prototype._bind = function () { // 这就是dirs[i]._bind()方法代码,元素节点每个属性指令都有一个directive实例都有一个通用的bind初始化方法
  // setup directive params
  this._setupParams();
  this.bind(); //每个指令实例中的bind方法是specific bind方法,通用_bind方法调用specific bind方法,每个指令都不一样。
    var watcher = this._watcher = new Watcher(this.vm, this.expression, this._update, // 凡是要获取表达式/变量值都要创建watcher,会把watcher加入到在求值过程中依赖的变量属性的dep中。
    this.update(watcher.value); //更新属性指令网页数据,比如id="$index"要把变量值插入,比如{{item.name}}要把变量值插入,网页数据更新之后就处理完了,更新网页是最后一步,每种属性指令的update方法都不一样。

可见vue处理指令的关键成分是watcher和指令实例中的bind/update初始化函数方法,在编译阶段处理所有的指令属性,new directive实例,保存起来,在link阶段只要把所有保存的指令实例都执行一遍bind方法进行初始化就完成了所有的指令初始化工作,包括标签组件也是要走指令流程的,只不过标签组件还要走一遍component处理流程,要new Vuecomponent()实例执行构造函数从_init()开始把compile()流程再走一遍完成组件template的编译,组件template中可能又有指令属性,就又要把上述流程走一遍完成对指令属性的编译处理,比如v-for="item in items"。

所以vue的通用指令处理程序和组件处理程序会从编译根组件开始被反复执行多次,递归所有的子组件,所有的子节点,层层嵌套,每个指令每个子组件都要执行一遍核心编译处理程序,如果在核心编译程序debug看,会有几十几百次输出,能看到所有的指令/组件的数据,还是非常复杂的。

以v-for指令为例来看一段指令的源代码:
var vFor = { //
  bind: function bind() {
    if ('development' !== 'production' && this.el.hasAttribute('v-if')) {
      warn('<' + this.el.tagName.toLowerCase() + ' v-for="' + this.expression + '" v-if="' + this.el.getAttribute('v-if') + '">: ' + 'Using v-if and v-for on the same element is not recommended - ' + 'consider filtering the source Array instead.', this.vm);
    } //不推荐同时使用v-for和v-if是在这儿检查告警的
    this.start = createAnchor('v-for-start'); //text节点
    this.end = createAnchor('v-for-end');
    replace(this.el, this.end); //先用占位元素替换掉v-for元素,等编译好v-for元素之后再插入网页到占位元素位置生效
    before(this.start, this.end);
    this.factory = new FragmentFactory(this.vm, this.el); //此过程会compile(this.el)生成linker保存在fragment实例中

      function FragmentFactory(vm, el) {
        linker = compile(template, vm.$options, true); // 编译指令template,v-for指令是一个数组循环产生一个列表


router切换路由组件时涉及transition过渡效果,所以相关代码是封装在transition中:
var startTransition = function startTransition() {
  transition.start(function () {
  _this3._postTransition(route, state, anchor);

    RouteTransition.prototype.start = function start(cb) {
      var view = this.router._rootView; //_rootView是<router-view>指令实例,里面有app组件构造器
      activate(_view, transition, depth, cb); //transition里面有要切换的子页面路由和component构造函数对象,里面有options(组件代码)

        function activate(view, transition, depth, cb, reuse) { //depth对应子路由
          var handler = transition.activateQueue[depth];
          if (!handler) {
            view.setComponent(null);
            component = view.build({ //view就是占位标签router-view指令实例,build方法就是var component={}中的build方法

              build: function build(extraOptions) {
                var child = new this.Component(options); //options就是之前准备好的组件构造器含组件代码,这就是new 路由组件实例化
                return child;

                  Vue[type] = function (id, definition) { //这就是vue component的构造函数的构造函数
                    definition = Vue.extend(definition);
                    this.options[type + 's'][id] = definition;
                    return definition;

view.transition(component);
  component.$before(view.anchor, null, false);

  //Actually swap the components
  transition: function transition(target, cb) { //var component={}中的transition方法,target是组件实例,实例中el是已经编译产生的DOM对象,可以直接插入网页生效。
    self.remove(current);
    target.$before(self.anchor, cb);

      Vue.prototype.$before = function (target, cb, withTransition) { //此时target变为anchor占位元素(text节点)
        return insert(this, target, cb, withTransition, beforeWithCb, beforeWithTransition);

          function insert(vm, target, cb, withTransition, op1, op2) {

            function beforeWithCb(el, target, vm, cb) {
              before(el, target);
              if (cb) cb();
            }

            function beforeWithTransition(el, target, vm, cb) {
              applyTransition(el, 1, function () {
                before(el, target);
              }, vm, cb);


有两个细节提一下:
function View (Vue) {
  Vue.elementDirective('router-view', viewDef);

在View构造函数中定义了router-view这个指令/组件。


下面是vue的on方法,是用底层js事件方法:
function on(el, event, cb, useCapture) {
  el.addEventListener(event, cb, useCapture);
}

vue没有再定义一套事件机制,就是从上层封装直接调用底层js方法来定义事件绑定,比肩简单。
要注意,cb中已经引用组件实例,所有的组件实例都是保存在根组件实例中,按id索引。当点击页面执行方法时,页面并没有作用域指引,方法也不在全局空间,
就是在js的events[]中找handler执行,关键关键关键是handler方法代码中已经引用组件实例,这就是作用域。
它如何引用组件实例呢?估计很少有人想过这个问题。
先new router实例,要么在全局空间创建router实例,要么把router实例保存在全局,再new 根组件实例,保存在router实例,再new 组件实例,保存在根组件,组件实例的方法用this引用组件实例,因此引用组件实例就是引用保存在全局的router实例中的属性,js对象引用机制会导致能引用
到组件实例,这个可是很关键的,否则就全乱套了。


其它的框架大都自己又定义了一套事件机制,就复杂了。

组件构造函数Vuecomponent用Vue.extend继承vue构造函数,每个组件只是options不同,源代码中组件的初始化方法:
var component = {
  bind: function bind() {
    this.setComponent(this.expression);

      build: function build(extraOptions) { //入口参数就是组件的代码
        extend(options, extraOptions);
        var child = new this.Component(options); //就是new VueComponent(),就是new Vue()实例,只不过是子实例。

      setComponent: function setComponent(value, cb) {
        self.mountComponent(cb);

          mountComponent: function mountComponent(cb) {
            var newComponent = this.build();


路由组件与页面组件不同,页面组件在编译template时会创建一个directve指令实例,里面有组件的id和descriptor,而所有的组件定义代码都在components[]中可以找到。
路由组件是new Vuecomponent()创建组件实例时执行_init() -> $mount(el)开始编译template,指令组件是在编译template过程中层层递归扫描html元素节点找到指令标签再编译处理指令标签。

 

根组件实例只在执行入口初始化时创建一次,每次路由变化时,会执行一次_match,this.app指根组件/主模块,根组件实例创建之后一直存在,不会再执行new _appConstructor()初始化根组件。

vue处理的对象主要是组件和指令,主要用vue构造函数和directive构造函数来处理组件和指令,new实例时就是初始化组件和指令,编译组件时从根节点开始递归循环
扫描所有的子节点,只要有组件/指令都是用同样的通用代码处理,所以只要上述两个构造函数对象写ok了,把编译循环递归写ok了,就基本成功了,不管template有
多复杂有多少层节点嵌套,有多少层组件嵌套,都是用通用方法和循环递归处理。
还有watcher构造函数,是为了实现数据更新同步。

Vue内置指令router-view,slot,v-for,v-if,v-model,是重要指令,每个指令有一套方法比如bind,update,都已经设计好,如果自定义指令,需要自己写方法,最起码
要写bind方法(初始化方法)。

从初始化根组件开始执行vue._init,如果根元素页面有调用组件,则每个组件都再执行一遍vue._init,从根组件开始每个组件
都初始化一个组件实例,从根元素开始每个节点都要编译处理一遍,编译之后产生link函数再执行link函数。对于每个属性指令
则执行一遍new Directive()创建一个directive实例,相关数据方法都保存在实例中,主要是el和update方法,
如果属性指令写在组件标签元素内,则el是编译之后插入网页的<div>元素,执行update方法更新网页时是针对网页中这个<div>
进行属性修改,指令实例保存在所属的组件实例中,路由组件中定义/调用的子组件都保存在组件实例的options.components下。

页面每个表达式都都会创建watcher,组件中被页面绑定的每个属性变量都会创建set/get方法,包含所有依赖自己的watcher,
watcher中的update方法会调用指令的update方法更新页面,比如:class指令的update方法就是修改所在元素的class属性,
class="{{}}"也是指令,更新时也是要修改所在元素的class属性。
vue自带指令保存在options.directives中,对于<test>这样的自定义标签,是已经用component定义的组件,它是指令组件,
既是指令也是组件,在当前页面组件实例的options.components下有,在_directives下也有,指令组件要把component流程
和directive流程都走一遍。


编译一个节点/属性,只是准备数据,产生link函数,执行link函数才初始化生效,比如执行指令的初始化方法,
对于组件指令,初始化方法最后要执行mountComponent才把template元素插入网页生效,之前的所有处理都是在做数据准备


每个指令都是事先定义好的,有bind/update方法,自定义指令也有方法,在页面调用指令时,
编译时查指令集合,获取指令的相关数据,然后new directive创建一个指令实例,保存在当前子页面组件实例中,编译就
大功告成了,然后执行link函数,把当前页面组件中调用的所有指令_directives[]都执行一遍初始化方法即可,指令的初始化
方法也是更新方法,就是是设置/修改指令所在的元素的属性,对于组件指令,就更复杂,要new component()创建组件实例,
再执行组件指令的初始化方法,执行mountComponent完成最后一个环节,把组件template元素插入网页生效,所以组件指令
既是组件又是指令,component和directive两种流程都要走一遍,非常复杂。在组件指令写属性,处理起来是最复杂的,
要把<test>变成<div>,标签原始属性复制到<div>,但标签原始属性的作用域是调用标签的页面组件,也就是标签组件的父组件,
而<div>中replacer属性,也就是写在template中的<div>中的属性,作用域是标签组件本身,除此之外,所有组件/指令处理
流程都是一样的,只是不同的组件/指令,它们的数据方法不同。


再小结一下:

组件走Vue构造器流程,指令走Directive构造器流程,组件指令两个流程都走,看Vue和Directive代码,要清楚每个组件,每个指令,都要执行‘
一遍同样的代码,代码会被多次执行,每次执行都是针对一个不同的组件/指令。编译方法中compileNodesList要递归所有的子节点,包括看不见的文本节点,
compileNode编译每个节点时要遍历所有的属性,html属性,指令属性,props属性,处理方法是不同的。组件指令既是组件又是指令,两种流程都要走一遍。
组件/指令都是事先定义好的,数据方法都已经有了,都保存在根组件Vue实例中,再继承到各个子组件实例中,编译时就是要判断标签/属性是不是已知指令,
如果是,就把数据准备一下,然后new directive实例保存在当前组件实例中,再执行link函数,link函数会执行指令的初始化函数,初始化函数会最后完成
网页更新,对于组件指令就是要把template元素插入网页生效。
为了实现数据同步,还要针对组件的属性和页面绑定的变量包括方法创建watcher,在初始化指令时要进行watcher相关处理,这是另外一种主要的流程,贯穿在
所有主要流程中。



vuex/store是vue作者参考flux/mutation实现原理自己写了一套代码实现了缓存数据方法,并不是第三方插件,调用
vuex/store初始化缓存的方式就是:

new Vuex.Store({
  modules: {
   userInfo: {
    state: {
      userInfo : {
        username: ''
      }

    }
  },
  mutations: {
    ['SETUSERINFO'] (state, data) {
      state.userInfo.username = data.username;
    }
  }
}
},


组件调用vuex/store数据方法的方式是:
import {getMenus} from '../vuex/getters/user_getters'
import {setUserInfo} from '../vuex/actions/user_actions'
store : store,
vuex : {
  getters : {
    getMenus : (state) => {
      return state.userInfo.globle.menus;
    }
  },
  actions : {
    setUserInfo : ({dispatch},data) => {
      dispatch('SETUSERINFO',data);
    }

  }
}

vuex安装到vue的相关源代码:
function vuexInit() {
  var actions = vuex.actions;
  options.methods[_key] = makeBoundAction(this.$store, actions[_key], _key); // 给action方法增加一层封装,所以你debug看组件实例里面的action方法并不是你自己写的原始方法,而是一个封装方法,任何框架只要访问全局store数据本质上都是加一层封装,由封装方法再调用真正的action方法,由于封装方法在store相关的程序空间,可以访问store/dispatch,可以传递给原始action方法,因此你写的原始action方法无需涉及store/state,可以写形参dispatch,实际调用时会自动传递dispatch,这就是action方法的奥秘,react的reducer/action方法本质原理上也是一样的。


store.js:
Vue.use(Vuex); //初始化时会安装vuex插件到vue,也就是执行vuex的install程序。

vuex.js:
function install(_Vue) {
  override(Vue); //改写vue
}
  function override (Vue) {
    var _init = Vue.prototype._init;


      Vue.prototype._init = function () { //改写vue原来的_init方法,其实还是调用原来的_init,但给options增加init=vuexinit方法
        var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
        options.init = options.init ? [vuexInit].concat(options.init) : vuexInit;
        _init.call(this, options);

          Vue.prototype._init = function (options) {
            this._callHook('init'); //会调用options.init,也就是调用vuexinit方法

              Vue.prototype._callHook = function (hook) {
                this.$emit('pre-hook:' + hook);
                var handlers = this.$options[hook];
                if (handlers) {
                  for (var i = 0, j = handlers.length; i < j; i++) {
                    handlers[i].call(this);

因此vuex扩展到vue之后,在new Vue(options)创建组件实例时,会执行vuexinit方法处理options中的vuex:{}表达式,会在组件中创建setUserInfo方法,
创建的方法代码是在原始action方法加了一层封装。


vue用set/get方法实现数据响应,其关键代码在于:
function defineReactive (obj, key, val) {
  var dep = new Dep()
  var childOb = observe(val) //如果属性有层次,要递归处理每一层属性

  Object.defineProperty(obj, key, { //这是针对数据对象和里面的属性创建set/get,是为了实现同步功能,
//组件定义的数据属性在new Vue()时复制到this._data。
    enumerable: true,
    configurable: true,
    get: function(){
      if(Dep.target){ // 在针对页面表达式创建new watcher()时会调用get方法取值,并把new watcher实例保存在Dep.target,Dep.target就是当前正在创建的watcher实例,这个地方挺不容易看懂的,因为要懂整体设计逻辑才能看懂,
        dep.addSub(Dep.target); //把watcher实例保存到属性对应的dep中,因此依赖obj的属性的watcher都会保存到属性中
        Dep.target = null;

      }
      return val
    },
    set: function(newVal){
      var value = val
      if (newVal === value) {
        return
      }
      val = newVal
      childOb = observe(newVal)
      dep.notify() // 执行属性中保存的watcher的update方法更新组件/指令的页面
    }
  })
}

其中dep的方法层层封装非常绕,详细代码本文忽略。

vuex/store也是用get/set方法实现数据响应,是通过加了一层computed方法实现的,computed方法再调用getter方法访问store的属性数据,computed方法会创建
watcher,当调用vuex getter方法获取属性的值时会把自身watcher实例保存在属性中,当set属性时就会执行watcher的update方法更新data属性,页面可以绑定
data属性,当data属性变化时,页面会更新,因为vue已经针对页面绑定表达式创建了watcher,针对data属性创建了get/set方法,已经建立了数据响应机制。
所以当使用computed方法时,还是很复杂很绕的,当数据变化时,从store到页面是通过好几层watcher实现数据响应的,页面表达式有一层watcher,cb是指令的
update方法,比如对于{{title}}这样最简单的表达式其实就是一个文本指令,其update方法就是要取title的值插入做为文本内容,computed方法又有一层watcher,
cb=null,因为computed方法只是执行方法代码获取值,不同于指令,没有cb,无需处理网页元素。

下面是vue构造comptedGetter方法的代码:
function makeComputedGetter(store, getter) {
  var id = store._getterCacheId;
  if (getter[id]) {
    return getter[id];
  }
  var vm = store._vm;
  var Watcher = getWatcher(vm);
  var Dep = getDep(vm);
  var watcher = new Watcher(vm, function (vm) { // 创建watcher,在执行getter方法取值时把自身保存到属性中
    return getter(vm.state);
  }, null, { lazy: true });
  var computedGetter = function computedGetter() { //computedGetter
    if (watcher.dirty) {
      watcher.evaluate();
    }
    if (Dep.target) {
      watcher.depend();
    }
    return watcher.value;
  };
  getter[id] = computedGetter;
  return computedGetter;
}

watcher实例里面,proto里面有一个update方法,这是watcher通用update方法,还有一个cb,这是指令通用update方法,
指令通用update方法会调用指令specific update方法(如果有的话)。


下面是关于在组件标签用v-on:绑定事件的代码细节:
//Register v-on events on a child component
function registerComponentEvents(vm, el) {
  name = attrs[i].name;
  if (eventRE.test(name)) {
    handler = (vm._scope || vm._context).$eval(value, true); //v-on=后面的写法需要解析处理一下
    handler._fromParent = true; //事件在当前初始化的组件,方法在父组件
    vm.$on(name.replace(eventRE), handler); // 绑定逻辑事件,逻辑事件用$emit触发

事件用addeventlistener和$on两种方法绑定,如果是物理事件,比如物理click事件,前者有效,如果是逻辑事件,前者
无效,后者有效,后者是vue自己建立的_events[]数据,handler方法在父组件。
在标签写v-on只能绑定组件里面的逻辑事件,组件里面用$emit触发逻辑事件,执行父组件里面的handler方法,不能监听子组件里面的物理click事件,
在子组件里面才能写v-on绑定物理click事件。



下面是关于checkbox元素用v-model双向绑定的源代码分析,checkbox handler代码:
var checkbox = {
  bind: function bind() {

    this.listener = function () {
      var model = self._watcher.get(); //获取v-model=表达式的值,也就是变量属性值option.checked,涉及v-for循环变量/scope
      if (isArray(model)) {
      } else {
        self.set(getBooleanValue()); //获取元素checked属性值,传递给set方法
      }

Directive.prototype.set = function (value) {
  if (this.twoWay) {
    this._withLock(function () {
      this._watcher.set(value);

        Watcher.prototype.set = function (value) {
          this.setter.call(scope, scope, value); //调用变量属性的setter方法设置值

            function (scope, val) { //watcher的setter
              setPath(scope, path, val);

                function setPath(obj, path, val) {
                  obj[key] = val;
                  set(obj, key, val);//如果属性不存在,就调Vue.set添加属性?
                  this.getValue = function () {
                    return el.hasOwnProperty('_value') ? el._value : self.params.number ? toNumber(el.value) : el.value;
                  };

                    function getBooleanValue() {
                      var val = el.checked;
                      if (val && el.hasOwnProperty('_trueValue')) {
                        return el._trueValue;
                      }
                      if (!val && el.hasOwnProperty('_falseValue')) {
                        return el._falseValue;
                      }
                      return val;
                     }
                  this.on('change', this.listener);


可见v-model底层就是绑定'change'事件,就是获取元素checked属性值,可以重新定义checked属性值,那就取定义的值,一般不。
当点击checkbox元素时,其checked属性会变化,v-model获取元素checked属性值同步到变量,反之则是根据变量值设置元素
的checked属性值,从而实现两个方向的数据同步。

vue初始化组件时处理template的代码:

function transcludeTemplate(el, options) {
  var template = options.template;
  var frag = parseTemplate(template, true);

    function parseTemplate(template, shouldClone, raw) {
      if (typeof template === 'string') {
        //template如果写#id,则去网页按id找元素,这已经是DOM对象了
        //如果template是html字符串,则用innerHTML编译为DOM对象再插入到frag
        frag = stringToFragment(template, raw);

            function stringToFragment(templateString, raw) {
              node.innerHTML = prefix + templateString + suffix; //前后缀是有可能包裹一层标签
              while (child = node.firstChild) {
                frag.appendChild(child);
                //这是移动子元素,循环结果把所有子元素都移动插入到frag中去,template可以写多个<div>

                  function nodeToFragment(node) {
                    // script template,template可以写在网页的<script>标签中
                    if (node.tagName === 'SCRIPT') {
                      return stringToFragment(node.textContent);
                    }

因此在组件中写template=只能是html代码字符串,或者#id指向网页中的元素,没有其它写法,不能写js表达式,
也不能用js代码修改template,按id找元素可以把template写在网页的<script>中,类似angular,也可以写在网页中的
<template>标签元素中:
<template id="t1">
<div>{{context}}</div>
</template>

如果构造<template>插入网页,<template>类似frag,本身是虚拟节点,在网页不占节点。

 

本文到此就差不多结束了,本文写的比较粗糙,因为vue 1.0源码是以前研究的,当时没有很好地整理出来,后来复制了几段源代码分析记录,大概编辑了一下就此发表,总比烂在肚子里强,本人对vue 1.0和2.0进行了深入的研究,进行了无数次debug看数据,因为本人认为vue非常优秀实用,值得学习,而angular和react太高大上,一般人没法学习,文中错误欢迎批评指正交流。

 

posted @ 2018-05-08 11:54  pzhu1  阅读(406)  评论(0编辑  收藏  举报