vue 1.0源代码重点难点分析

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

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

function Router() {} // router构造函数
var router = new Router(); // new router实例
router.start(app); // 项目真正的入口

Router.prototype.start = function start(App, container, cb) {
  this._appContainer = container; //把app和container保存在router实例,后面的Vuecomponent就是app。
  var Ctor = this._appConstructor = typeof App === 'function' ? App : Vue.extend(App);
  this.history.start();

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

onChange: function onChange(path, state, anchor) {
  _this._match(path, state, anchor);

Router.prototype._match = function _match(path, state, anchor) {
  var _this3 = this; // this是router实例
  var route = new Route(path, this);
  var transition = new RouteTransition(this, route, currentRoute); //new transition实例时传入router实例
  if (!this.app) {
    _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) {
      var nodeLinkFn = compileNode(el, options)
      var childLinkFn = compileNodeList(el.childNodes, options)
      return function compositeLinkFn(vm, el, host, scope, frag) {

          function makeChildLinkFn(linkFns) {
            return function childLinkFn(vm, nodes, host, scope, frag) { //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);
  }
  vm._bindDir(descriptor, el, host, scope, frag);
};

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


Directive.prototype._bind = function () { //dirs[i]._bind(),元素节点每个属性指令都有一个directive实例
  // setup directive params
  this._setupParams();
  this.bind(); //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方法都不一样。

以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,本身是虚拟节点,在网页不占节点。

猜你喜欢

转载自www.cnblogs.com/pzhu1/p/9007441.html
今日推荐