Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue插件机制及模板渲染 #2

Open
Cyrilszq opened this issue Jan 15, 2017 · 5 comments
Open

vue插件机制及模板渲染 #2

Cyrilszq opened this issue Jan 15, 2017 · 5 comments

Comments

@Cyrilszq
Copy link
Owner

Cyrilszq commented Jan 15, 2017

接触Vue也有一段时间了,Demo也写过几个,个人还是比较喜欢Vue的。记得当时用的时候就对三个问题比较感兴趣——1.响应式原理;2.插件如何工作;3.组件的实现。其中关于探究响应式原理的文章网上不少,包括模仿Vue基于Object.defineProperty实现数据绑定的简单实现也有不少,比如这几个

不过这两个的Compiler部分都是基于1.x的,2.x引入了虚拟DOM,关于这个的整个流程,下面会有简单总结。至于第二问题,大概搜了一下,估计是太简单了(弄清楚之后发现确实不复杂,就是在生命周期中去调用自定义的插件,复杂的是整个生命周期流程)没什么人写这方面的东西,只好对着文档源码(2.0.0版的)自己去看看了。

Vue插件机制

使用插件前要调用Vue.use(MyPlugin),这就相当于调用MyPlugin.install(Vue)

Vue.use = function (plugin: Function | Object) {
   /* istanbul ignore if */
   if (plugin.installed) {
     return
   }
   // additional parameters
   const args = toArray(arguments, 1)
   args.unshift(this) //使第一个参数变为Vue,剩下的直接传过去
   if (typeof plugin.install === 'function') {
     plugin.install.apply(plugin, args)
   } else {
     plugin.apply(null, args)
   }
   plugin.installed = true
   return this
 }

所以插件都要有install方法。

文档举了这几种开发插件的方式

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或属性
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }
  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })
  // 3. 注入组件
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })
  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (options) {
    // 逻辑...
  }
}

第一种和第四种比较简单,直接把方法挂在Vue或是Vue原型上,作为全局方法或实例方法,和jQuery差不多(不过jQuery的extend方法还有其他功能),在vue里似乎用的很少。第三种通常通过全局mixin方法在vue生命周期中注入一些初始化插件的代码,如vuex。

Vue.mixin = function (mixin: Object) {
    Vue.options = mergeOptions(Vue.options, mixin)
}

可以看到mixin方法可以传入一个对象,如传入{ beforeCreate: vuexInit },则经过mergeOptions后会将vuexInit函数与vue的beforeCreate生命周期钩子函数关联起来,mergeOptions并不是简单的替换,因为那样会覆盖原来的钩子函数,对于混合对象与组件存在同名生命周期方法时,vue会将他们都保存在一个数组中,并且混合对象的方法会先执行。例如vuex是这么用的:

  const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
  //1.0用钩子函数init,2.0用beforeCreate
  Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }

这样Vue生命周期走到beforeCreate的时候就会调用vuexInit,完成$store的挂载(vue-router也有类似的操作)。

感觉功能最强大用得最多的是第二种,也就是自定义指令,很多组件库都带有一些自定义指令,之前我自己也写了个Demo,关于图片懒加载指令的,指令用起来确实很方便。

调用Vue.directive()的时候仅仅把自定义指令添加到Vue.options.directives上,这样后面生成vnode的过程中将所有的自定义指令转换成一个对象才能顺利处理,然后根据生命周期调用相应的钩子函数。

关于全局方法的初始化,如directive,component包括之前的use,mixin都在 src/core/global-api 下

_assetTypes: [
  'component',
  'directive',
  'filter'
]

config._assetTypes.forEach(type => {
  Vue[type] = function (
    id: string,
    definition: Function | Object
  ): Function | Object | void {
    if (!definition) {
      return this.options[type + 's'][id]
    } else {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production') {
        if (type === 'component' && config.isReservedTag(id)) {
          warn(
            'Do not use built-in or reserved HTML elements as component ' +
            'id: ' + id
          )
        }
      }
      if (type === 'component' && isPlainObject(definition)) {
        definition.name = definition.name || id
        definition = this.options._base.extend(definition)
      }
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition }
      }
      this.options[type + 's'][id] = definition //这里
      return definition
    }
  }
})

模板到DOM大致流程

关于模板到真正DOM的大致流程,写了个demo跑了一下,大概是这样的,首先template模板经过parse处理后返回一棵AST,即

/**
 * Convert HTML string to AST.
 */
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  //....省略大量代码,具体实现在src/compiler/parser 
}

获得一棵AST后再经过generate()生成渲染函数

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): {
  render: string,
  staticRenderFns: Array<string>
} {
  //...省略,在src/compiler/codegen/index.js
}

执行渲染函数后会获得一个VNode,即虚拟DOM,然后把它交给patch函数,负责把虚拟DOM变为真正DOM。

例如在chrome打断点跑一下这个模板

<body>
<div id="app">
    <h1 v-test="message"></h1>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue!'
        }
    });
</script>
</body>

parse后返回
screenshot from 2017-01-14 21-12-41

generate后返回

return {
  // 渲染函数,执行后生成vdom
    render: ("with(this){return " + code + "}"), //code =_c('div',{attrs:{"id":"app"}},[_c('h1',{directives:[{name:"test",rawName:"v-test",value:(message),expression:"message"}]})])
    staticRenderFns: currentStaticRenderFns
}

执行渲染函数后生成的VNode
screenshot from 2017-01-15 22-04-39

然后经过patch变成真正DOM,期间还有个很重要的Wather,用来收集依赖,数据发生变动时触发diff,这块的原理基本和vue1.x没什么变化。

小结

其实关于Vue的实现过程我现在只想了解个大概,对整个设计思路有个印象,这样选择其他库 框架的时候也有个比较。而且毕竟水平有限,全部看源码太费力又没那么多时间。

参考资料

@Vi-jay
Copy link

Vi-jay commented Mar 21, 2017

大神 , 请问一下 模板渲染也是属于DOM操作的一种吗 DOM操作的reflow会使页面变的很卡 但是我看vue ng react这些就不会 是为什么呢

@Cyrilszq
Copy link
Owner Author

@Vi-jay 模板渲染和DOM操作没什么关系吧,我这里的模板指的是.vue文件中的template,讲的是template如何变成真实DOM的,如果非要说联系,最后一步patch确实要操作DOM。

只有频繁的reflow或者DOM结构非常复杂(比如大列表)更新,才会很卡。而vue ng react这些比较快是因为它们通过各种机制来减少不必要的DOM操作,比如vue,react都有批量更新DOM的机制,而且它们通过虚拟DOM和高效的diff算法来精确的知道哪些DOM结构是要改变,还有DOM复用等等。

@Vi-jay
Copy link

Vi-jay commented Mar 22, 2017

所以说它们也是操作DOM来更新节点的? 类似于Fragment这种批量的DOM操作?

@Cyrilszq
Copy link
Owner Author

@Vi-jay 是的,你说的Fragment也是批量的一种,不过我上面提到的批量更新DOM不是指这个。
而是指文档中提到的异步更新队列,关于这块是如何实现的你可以看我的这篇博客

@Vi-jay
Copy link

Vi-jay commented Mar 22, 2017

好的,多谢大神指点

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants