Git Product home page Git Product logo

blog's People

Contributors

holocc avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

blog's Issues

vue component 初始化

VNode

一段简单 template 代码,如 <div a=1><child/></div>,在经过 vue compiler 处理后,生成的 render 函数是这样的:

function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "a": "1"
      }
    }, [_c('child')], 1)
  }
}

这里的 _ccore/vom/create-element 中的 createElement ,从函数声明中也可以知道,该函数的作用是返回 VNode,在后续的 patch 过程中使用。其他在 render 中使用的工具函数位于 core/instance/render-helpers

那 vue 是如何区分普通 DOM 节点和组件的呢?
怎么知道 div 是普通的节点,而 Child 是组件。

// core/vdom/create-element.js
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
	if (typeof tag === 'string') {
    let Ctor
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
			// core/vdom/create-component.js
      vnode = createComponent(Ctor, data, context, children, tag)
    }
  }
}

判断 tag 字符串是不是保留元素(HTML 和 SVG 中的 tag),来判断是什么类型。

普通节点

vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )

Vue 组件

vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }, // componentOptions 选项
    asyncFactory
  )

可以从 VNode 的创建中看出一些不同点:

  1. 组件在 vnode 中没有 children 选项
  2. 组件多传了 componentOptions 参数
    1. 包含 vue 构造函数 props 监听器 tag children 子节点

实现细节

// src/vdom/create-component.js
function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
	data = data || {}
	// 利用 options.props 选项,从 data 中提取 propsData
	const propsData = extractPropsFromVNodeData(data, Ctor, tag)
	
	// 从 data.on 中提取父节点传递的 listeners,事件监听器
	// 对于组件来说,@click 并不是原生事件,而是通过 $on $emit API 模拟的事件
  const listeners = data.on
	// 组件中,native 事件会在组件根元素上监听一个原生事件
  data.on = data.nativeOn

	// data.hook = {}
	// 在 hook 中增加 componentVNodeHooks
  // 增加 init,prepatch,insert,destroy 钩子,在不同的时机中会被调用
	installComponentHooks(data)
}

patch

if (isUndef(oldVnode)) {
      createElm(vnode, insertedVnodeQueue)
    } else {
      if (sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
		}
}

简单来说 patch 有两种情况

  1. 旧节点不存在,就使用 VNode 创建真实 DOM 元素
  2. 同一个 VNode ,调用 patchVNode 进行更新

创建

createElm 中调用 core/vdom/patch.js 中的 createComponent 创建元素

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      // 1. 调用 vnode 过程中安装的 componentHooks
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }
			
      // 2. 初始化组件,插入到 DOM 中
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

init 钩子

  1. createComponentInstanceForVnode 为 vnode 创建 componentInstance
    1. vnode 中两个与 vue 实例相关的属性为 componentOptionscomponentInstance
    2. componentOptions 在创建 vnode 时生成
    3. componentInstancepatch 过程中生成,新建一个 vue 实例
  2. 空挂载

创建组件实例

组件本质还是一个 Vue 实例,但是与 root 实例初始化过程中存在一些差异,所以在流程上也有一些区别。比如,根实例不需要从父组件获取数据,而组件需要处理 propsData

function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  debugger
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
	
  // 使用 Ctor 构造函数创建一个 vue 实例
  // 组件原本的 options 存到 Ctor.constructor 中
  // 等同于 new Vue() 
  // 由于是组件,通过 _isComponent 选项,走不同的 init 过程
  return new vnode.componentOptions.Ctor(options)
}

构造函数

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._isVue = true
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    }
}

function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

如果是组件,会到 vnode 的 componentOptions 中得到一些数据,然后继续走正常的初始化流程。

vue-loader

思路

webpack 只能处理 JS 和 JSON 文件,其余的文件类型需要依靠 loader 处理。vue-loader 是怎么处理 vue 文件的呢?

参考 vue-loader

  1. 将 vue 文件中的 template script style 分为三个块 (block) 分别引入
    1. 被 loader 处理过的文件,都会携带一些 query,进行标识
  2. 按照 webpack 中的配置处理特定的 block
    1. 比如配置中使用 sass-loader css-loader style-loader 处理 css,那么将使用这几个 loader 对 style block 进行处理
// code returned from the main loader for 'source.vue'

// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'

script.render = render
export default script	

实现

先单独了解 vue-loader 流程中一些模块的职责

plugin

使用 vue-loader 时,需要在 webpack 配置中添加 VueLoaderPlugin

  1. 克隆一份 webpack 配置中的 rule,增加 resouceQuery 来判断是否为 vue 生成的文件引入,vue-loader 处理过的文件都带有 vue query
  2. 生成 pitcher-loader
    1. 拦截所有带 vue query 的请求
    2. 获取当前请求匹配的 loader
    3. 生成 inline loader 请求
  3. 返回增加 1、2步后的 loader rules

vue-loader

普通的 vue 文件请求

import './source.vue'

普通的 vue 文件使用 @vue/component-compiler-utils 处理后,分割为三个块,分别是 template style script

然后,对这三个块单独进行请求

// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
带 query 的 vue 文件请求

import render from 'source.vue?vue&type=template'

vue-loader 返回文件中的某个具体 block

pitcher-loader

根据 pitch 的特性,先会正向的执行 loader 中的 pitch 函数,如果 pitch 有返回值,就不会再执行剩余的 loader pitch 以及 loader 功能了。

在 plugin 中,生成了一个 pitcher,用于拦截所有带 vue query 的请求。然后用克隆的 rules 规则进行匹配,生成对应的 inline loader ,loader 将从右到左顺序执行

'-!../lib/loaders/templateLoader.js??vue-loader-options!../node_modules/[email protected]@pug-plain-loader/index.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&'

整体流程

  1. 请求 vue 文件
  2. 转为三个带 query 的请求
  3. pitcher loader 拦截,并生成 inline loader
  4. inline loader 处理源文件

scoped 实现

通过文件内容以及路径哈希值生成 id

  const id = hash(
    isProduction
      ? (shortFilePath + '\n' + source)
      : shortFilePath
  )

在 query 中将 id 传递给 template 以及 style 块

template

将 scopeId 添加到 vue options 中

Vue({
	render: ...,
	_scopeId: 'data-v-xxxxxx'
})

vue 在 patch 过程中的 createElm ,调用 setScope(vnode),将其添加到 dom 节点上

style

stylePostLoader 中使用 postcss 对 css 进行处理,增加修饰

其他

注意到导出 template 模块为 render 函数,实际上 vue-loader 会用vue-template-compiler 将模板编译为渲染函数。

这意味着,如果我们使用 Vue SFC 的方式进行开发,编译过程实际上是 vue-loader 做的,因此我们只需要引入 Vue 的运行时版本,这也是 Vue 的默认引入方式。

我们只需要保证,入口文件中也使用 render创建 Vue,而不是template

new Vue({
    render: h => h(App)
})

vue render 和 slot 的使用

slot

作用: 通过slot可以向子组件插入内容。

像子组件传递一些父组件创建的dom内容

具名插槽

可以通过具名插槽,分配多个插槽位置,但是必须存在一个默认插槽

也就是说,使用具名插槽时,意味着有多个名字的slot,这个时候必须有个名为deafultslot,或者<slot></slot>

Vue.component('test', {
    template: `
      <span>
        <header>
          <slot name="default"></slot>
        </header>
        <footer>
          <slot name="footer"></slot>
        </footer>
      </span> 
    `
  })

  new Vue({
    el: '#app',
    template: `
      <test>
        <template v-slot:header>
          <h1>Here might be a page title</h1>
        </template>
        <template v-slot:footer>
          <p>Here's some contact info</p>
        </template>
      </test>
    `
  })

作用域插槽

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

父组件通过slot向子组件传递DOM,通过作用域插槽,可以让父组件访问子组件的数据

Vue.component('current-user', {
    template: `
      <span>
        {{ user }}
        <slot v-bind:user="user"></slot> // 子组件传递数据
      </span> 
    `,
    data () {
      return {
        user: 'jiangkunhe'
      }
    }
  })

  new Vue({
    el: '#app',
    template: `
      <current-user>
        <template v-slot="slotProps"> //父组件使用子组件的数据
          {{ slotProps }}
        </template> 
      </current-user>
    `
  })

render

一个vnode对象使用的属性深入数据对象

render函数产生vnode,其中一个重要的函数是createElement

createElement

传递tag data 也就是数据对象 children 生成 vnode

new Vue({
    el: '#app',
    render (createElement) {
      return createElement('div', {
        on: {
          click: this.handleClick
        }
      }, [
        'helloworld'
      ])
    },
    methods: {
      handleClick () {
        console.log('handleClick run')
      }
    }
  })

单文件组件中的写法

可以像react一样,类似写脚本的形式使用

<script>
export default {
  render (h) {
    return (
      <span>
        { this.a } 
      </span>
    )
  },
  data() {
    return {
      a: 'fsjdkfjsdkl'
    }
  },
}
</script>

webpack 分离 chunk

我们使用webpack打包应用时,默认情况下一个入口最终产生一个JS文件。在使用vue-cli进行打包时,除了app.js之外,还有vendor.js

还有很常见的一种打包方式会将JS文件分为3个部分。

  1. app.js 业务代码
  2. vendor.js 第三方库代码
  3. manifest.js

这样做有什么好处呢?

在生产环境中,一般会为静态资源加上文件指纹。假如业务代码和第三方库代码打包到一起,那么任何一点代码的改动都会让文件指纹发生改变。

第三方库代码一般是不会改变的,假如将它单独提取出来,打包成一个文件,在库代码没有发生变化的情况下文件指纹就不会修改。这样做可以更好的利用客户端的缓存能力,减少请求次数。

抽取模块

在webpack3中,使用CommonsChunkPlugin

在webapck4中,使用的是SplitChunkPlugin

环境

目前webpack的版本为4.40.2。使用CommonsChunkPlugin时,将webpack的版本修改为3.3.0

// 项目结构
├── app.js
├── package.json
├── util.js
└── webpack.config.js
// app.js
import react
console.log('app.js')

// util.js
export function foo() {
	console.log('util.js')
}

CommonsChunkPlugin

下面是webpack的配置文件

// webpack.config.js
const webpack = require('webpack')
const path = require('path')
module.exports = {
  entry: {
    app: './app',
    // vendor 这个 Chunk 只包含了 react,而 app.js 中也同样使用了 react ,因此插件可以从中抽取出
    // 公共的部分, 也就是 react
    vendor: ['react']
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash].js',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function(module){
        return module.context && module.context.includes('node_modules');
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
  ],
};	

运行一下npx webpack --config webapck.config.js进行打包

第一次打包结果

Hash: bf0ff02fe4d883a5152e
Version: webpack 3.3.0
Time: 145ms
                           Asset       Size  Chunks             Chunk Names
  vendor.6d6ff8a174ebb5c83bba.js    93.8 kB       0  [emitted]  vendor
     app.dde83d0702db93ce46d5.js  450 bytes       1  [emitted]  app
manifest.5d0c9ed65d61958411f2.js    5.85 kB       2  [emitted]  manifest
   [3] ./app.js 40 bytes {1} [built]
   [8] multi react 28 bytes {0} [built]
    + 7 hidden modules

出现了三个文件,没有问题。

//app.js
import 'react'
import './util.js'

修改一下app.js,引入其他业务模块,再一次打包

第二次打包结果

Hash: d4cc66daafa987cbb978
Version: webpack 3.3.0
Time: 146ms
                           Asset       Size  Chunks             Chunk Names
  vendor.8d03c7d7bd91cf3f7ddc.js    93.8 kB       0  [emitted]  vendor
     app.1b91e0fcccbac051e6a1.js  725 bytes       1  [emitted]  app
manifest.368326d1543c92ebcaf9.js    5.85 kB       2  [emitted]  manifest
   [3] ./app.js 59 bytes {1} [built]
   [8] ./util.js 52 bytes {1} [built]
   [9] multi react 28 bytes {0} [built]
    + 7 hidden modules

值得注意的是,第三方库没有变化,可是vendor的指纹改变了。

在网上查了挺多相关配置,最终还是在官方文档中找到答案

// vendor.js
// 打开 vendor.js 两次打包中,id 发生了变化,导致 hash 改变
webpackJsonP([id1], [(function() {...}, ...)], [id2]) 

原因是,打包的过程中,webpack会给模块一个id,通过这个id来标识一个模块。当增加一个模块的时候,id的解析发生变化了,因此生成的vendor发生了变化。

官方给出的解决方案是,将id改为哈希的。

于是搜了一下有没有能让模块ID改为哈希值的插件,果真有——HashedModuleIdsPlugin,将这个插件加入到插件列表后确实达到了目的。这个hash值是根据什么来产生的呢?文档的开头也给了解释。

This plugin will cause hashes to be based on the relative path of the module, generating a four character string as the module id. Suggested for use in production.

根据模块的相对路径生成四个字符的哈希值,并且建议用在生产环境。

在webpack的插件中加入

new webpack.HashedModuleIdsPlugin()

两次打包的vendor指纹是不会发生改变的。

SplitChunkPlugin

webpack4中,已经不推荐使用CommonsChunkPlugin了。这篇文章对比了一下两个插件,说了CommonsChunkPlguin的一些缺点。

// webpack.config.js
module.exports = {
	// ...
  optimization: {
    runtimeChunk: 'single',
    moduleIds: 'hashed',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

SplitChunkPlugin可以通过配置模块引用次数、模块大小、正则去很方便的分割出一个新的Chunk

用之前的方法打包看看。在这个过程中,我发现没加moduleIds: 'hashed' 这一条时,已经达到效果,好像webpack的文档和实现已经不太同步了。

vendor文件中,模块不再以数字的方式标识id,而是改为文件的相对路径
比如 ./node_modules/[email protected]@object-assign/index.js

所以,当三方库的依赖没变化时,它的哈希值也不会发生改变。

总结

在学习webpack的过程中,要多动手实践。以后希望能腾出时间深入学习。

参考

vue-router

最近在学习vue-router,感觉比想象中复杂。

整体流程

根据流程大致画了一个思维导图,也可以参考滴滴前端博客中的流程图

vue-router 的安装过程

  1. 使用Vue.use,调用插件的 install 函数
    1. Vue.prototype中挂载方法,暴露接口, $router $route
    2. Vue.component 提供公共组件,router-view router-link
    3. 利用 mixin 混入生命周期和属性,beforeCreate _routerRoot _router
  2. new Vue() 的过程中传入选项
beforeCreate () {
  if (isDef(this.$options.router)) {
    // options 中传递了 router 选项,设置 _routerRoot 为自己
    this._routerRoot = this
    this._router = this.$options.router
    this._router.init(this)
    Vue.util.defineReactive(this, '_route', this._router.history.current)
  } else {
    // 取父组件的 _routerRoot
    this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
  }
}

vuex非常类似,在beforeCreate中判断当前组件的options 中有没有定义 router(也就是VueRouter实例),没有的话就取父组件的。

通过传递的方式,在所有组件**享了VueRouter

视图更新

视图更新依赖router-view组件。

vue-router提供了router-view组件用于展示视图,组件的render函数中使用了一个响应式数据_route,因此当_route变化的时候,会通知router-view组件进行更新,渲染相应的组件。

// _route 是一个 Route 对象
declare type Route = {
  path: string;
  name: ?string;
  hash: string;
  query: Dictionary<string>;
  params: Dictionary<string>;
  fullPath: string;
  matched: Array<RouteRecord>; // 根据当前路由路径匹配的 RouteRecord 数组
  redirectedFrom?: string;
  meta?: any;
}

declare type RouteRecord = {
  path: string;
  regex: RouteRegExp; // path-to-regexp 生成的正则
  components: Dictionary<any>; // 对应我们编写 VueRouter 中传的组件
  instances: Dictionary<any>;
  name: ?string;
  parent: ?RouteRecord;
  redirect: ?RedirectOption;
  matchAs: ?string;
  beforeEnter: ?NavigationGuard;
  meta: any;
  props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}

_route 对象更新的途径有两种:

  1. 通过调用vue-router提供的接口,如push go replace
  2. 浏览器事件,popstatehashchange。取决于vue-router的模式,监听不同的事件。

实际上,这两种方式最终都会调用transitionTo函数,该函数在路由成功跳转后,会通过history.listen 的回调形式更新_route,从而更新视图。

浏览器事件

在 Web 环境中,我们可以使用 hash history 两种路由模式,默认情况下会使用 hash 模式。

hash

当 URL 中的 hash 值改变时,就会触发 hashchange 事件,并且会留下记录。

history

通过 HTML5 提供的 History API 进行模拟,访问和操作历史记录,不会刷新页面,提供了主动改变浏览记录的能力。

当用户点击前进后退,或者我们调用 API 前进后退时,会触发 popstate 事件。IE 10 以上才支持。

阅读源码后发现,hash 模式并不一定监听hashchange事件。

export const supportsPushState =
  inBrowser &&
  (function () {
    const ua = window.navigator.userAgent

    if (
      (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
      ua.indexOf('Mobile Safari') !== -1 &&
      ua.indexOf('Chrome') === -1 &&
      ua.indexOf('Windows Phone') === -1
    ) {
      return false
    }

    return window.history && typeof window.history.pushState === 'function'
  })()

// 如果当前环境支持 Histroy API 就会使用 History API 进行模拟
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
  eventType,
  handleRoutingEvent
)
两个事件的一些差异

当使用History API 调用 pushState replaceState 时,不会触发popstate事件。而页面路由发生变化,一定会触发hashchange

在使用hash路由时,使用router API改变路由后,还会触发一次hashchange

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    // 相同的路由不会继续执行逻辑
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      return abort(createNavigationDuplicatedError(current, route))
    }

history的基类中,confirmTransition函数保证相同路由不会继续执行,因为路由跳转的过程中,会触发相应的钩子函数,以及执行路由守卫。

其他 API

scrollBehavior

有时候在一些详情页,只是替换了路由中参数 ID,复用了组件,然而滚动条会保持原来的位置。之前没有发现vue-router提供了滚动行为的接口,导致自己去监听$route变化重置滚动条。

命名视图

declare type RouteRecord = {
  path: string;
  regex: RouteRegExp;
  components: Dictionary<any>; // 对应我们编写 VueRouter 中传的组件
}

可以从RouteRecord的实现中看到,components是一个字典,也就是对象,这意味着在一个router-view中可以表示多个组件。

// 或许可以通过绑定属性,动态改变 name 显示不同的组件,还没有想到有什么实践方式
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>

参考

vue事件绑定原理

这是一个简单的 vue demo。

let vue = new Vue({
    el: '#app',
    template: `
      <div @click="handleClick('abcd')"></div> 
    `,
    methods: {
      handleClick (a) {
        console.log(a)
      }
    }
})

从 Vue 的整个流程思考,看Vue是如何将事件进行绑定的。

  1. vue 初始化
  2. 模板编译
  3. patch

Vue初始化

vue 初始化 _init 函数中,会调用 initEvents 初始化事件相关的动作。

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}	

每一个 vue 实例,创建了一个 _event 对象,这个对象实际上是给虚拟事件用的,并不是真实的 DOM 事件,使用$on在对象中添加事件,$emit进行触发。紧跟着,从options中拿_parentListeners,然后进行更新。

由于我们当前例子只会产生一个 vue 实例,先暂时忽略 _parentListeners

模板编译

由于我们给的是 template,vue 会将模板编译,产出 AST 和 render 函数

ast

// 生成的 AST 对象
{
  attrsMap: { @click: "handleClick('abcd')"},
  events: {
    'click': {
      value: "handleClick('abcd')",
    }
  }
}

模板编译后得到抽象语法树,树里包含了实例化一个真实节点的所有信息。比如当前 element 的属性,子节点 children 等等。这里只截取了一部分属性。

在 attrsMap 中可以发现,我们的事件和 style、class 这些真实属性没有区别,只是=分割开来,前面是 key,后面是 value。

因为对 html-parse 阶段来说,@click="handleclick('abcd')class="a b" 是没有区别的,都只是 html 中的属性,只是 vue 需要对这种属性做特殊的处理。

vue 通过正则/^@|^v-on:/判断,假如属性以@v-on开头,就是要进行事件绑定了。在 ast 对象中加上了 events ,并将 click 加到里面。

另外,当我们将模板修改为@click.once=handleclick('abcd')的时候,events 中生成的属性会变成~click

绑定属性中,once stop 这些称为 modifier。vue 针对事件监听中的modifier,做了特殊的处理,方便后续阶段进行相应的处理。

  • capture -> !
  • once -> ~
  • passive -> &

render

// render 字符串
with(this){return _c('div',{on:{"click":function($event){return handleClick('abcd')}}},[_v("fjskdflds")])}

字符串会通过 new Function(code) 的方式创建一个函数。

从字符串中可以看出,我们的click 事件函数 是 on 对象中的一个属性click。可以很自然的联想到,之后可能会用addEventListener('click', fn)去添加相应的函数(其实不是)。

同时,在渲染函数中,我们的代码有了一些变化。

click函数被包裹在一个带有$event变量的函数中。这也就不难理解,为什么我们可以在自己的模板字符串(如 handleClick('abcd', $event))中使用$event,从而得到原生的事件对象了。因为创建函数以后,这个变量在我们函数的作用域上层。

通过修改字符串模板,最后创建出来真实的函数,这种方式很神奇。

patch 阶段

render 函数生成 vnode。根据 vnode 进行 patch 的过程中,定义了一些钩子函数,如 create update。在 patch 的不同阶段进行调用,事件就是通过这些钩子函数绑定上去的。

这些钩子函数在/platforms/web/runtime/modules文件夹中,现在我们只关心 events.js

可以发现,在create update时,实际上都是将vnode传给updateDOMListener,这个函数负责了 DOM 事件的创建和更新。该函数实际上是/src/core/vdom/helpers/update-listeners.js

update-listener

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {}

函数遍历 on 对象,通过normalizeEvent函数处理特殊的属性名,将其转为参数,也就是once passive 等。

然后根据新旧 vnode 对比,更新、替换、删除事件函数。

// 最终的 vnode
{
	tag: 'div',
	data: {
		on: {
			click: function invoker() {}
		}
	},
}

实际上,我们事件函数会再被封装一次,包裹在一个名为 invoker 的函数中

  1. 该函数由createFnInvoker创建,将我们的函数包裹在一个异常处理代码块中执行。
  2. 我们的函数实际上实际上是invoker函数的一个属性fns,当事件触发时,调用的是 invoker,invoker 再找我们的函数。这样的话,当我们的事件函数变化时,只需要修改这个属性,不需要removeEventListener

parentListeners

再回到之前初始化的例子,做一点修改

Vue.component('child', {
    template: '<div>child</div>'
})
let vue = new Vue({
    el: '#app',
    template: `
      <child @click="handleClick('abcd')"></child> 
    `,
    methods: {
      handleClick (a) {
        console.log(a)
      }
    }
})

这个时候,我们的 click 事件是绑定在子组件上的。这就和真实 dom 元素的事件有区别了。

我们知道 vue 实例可以通过 $emit 触发事件,$on 绑定事件,父子组件之间可以进行通信,不需要使用浏览器的 API。

_parentListeners 就是父组件需要在子组件注册的函数。过程同样是调用updateListeners,区别就是后面的参数,add remove函数。之前的例子中,add 函数是 addEventListener,在这个例子中是 $on

其他问题

vnode是虚拟节点,什么时候将这个函数挂载到真实DOM节点中?

patch的时候会根据vnode创建真实DOM节点,并且将其赋值为elm到vnode中,通过这个引用,添加函数

function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  const on = vnode.data.on || {}
  const oldOn = oldVnode.data.on || {}
  target = vnode.elm
  normalizeEvents(on)
  updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
  target = undefined
}

函数调用的过程中,怎么保证this指向当前vue实例?

在init阶段,initMethods过程中,如果判断属性是函数,会将其bind到当前实例。

vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)

vuex

在 vue 项目中,父子组件通信是比较容易的,但是我们有时候也需要跨代之间的通信,兄弟组件的通信,有些数据是需要共享的。

就像文档中所说:

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。

从源码看,创建一个 Vuex.store 主要做了四件事

class Store {
  constructor (options = {}) {
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }
    
    this._modules = new ModuleCollection(options)
    
    installModule(this, state, [], this._modules.root)
    
    resetStoreVM(this, state)
  }
}
  1. 安装
  2. 注册 module
  3. 安装 module
  4. 用 store 创建一个 vue

安装

首先我们知道安装 vuex 有两步:

  1. Vue.install(Vuex)
  2. new Vue({ store })
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])
  Vue.mixin({ beforeCreate: vuexInit })  

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

vuex 安装过程中通过 mixin,为所有 vue 实例增加了 breforeCreate 函数,即 vuexinit

实例化 Vue 应用的时候,传递定义的 store,这意味着在根 Vue 实例中可以通过 $options 拿到 store

所以分成两种情况

  1. 根 Vue 对 store 进行实例化。
  2. 子组件通过 parent 拿到 store。

这样所有的组件都得到 $store,注入就完成了。

注册 module

const store = new Vuex.Store({ ...options })

我们创建 store 是通过传 options 的方式,因此需要对其进行处理,生成相应的数据结构。

我们传递的 options 是有 module 的层级关系的,一个 module 可以拥有子模块。

const store = new Vuex.Store({
  // 拥有子模块 a 和 b
  modules: {
    a: moduleA,
    b: moduleB
  }
})

ModuleCollection 做的事情就是将 options 转换成实际的 Module对象,并且维护他们的父子关系。实际上就是使用对象生成模块树。

Screen Shot 2020-03-28 at 10.55.00 PM.png

安装模块

安装模块就是将 module 中的 state getter action mutation 放到 store 中,后面我们就可以通过 store 调用定义好的接口。

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

代码非常整洁,做了四件事:

  1. 计算 namespce
  2. 将模块的 state 放到 store.state
  3. 遍历,将 mutation action getter 放到 store 中
  4. 递归注册子模块

命名空间 (namespce)

代码开头就获取了当前模块的 namespce,后续注册 mutation 等都和这个东西有关,可以说是非常重要的。

ModuleCollection 根据当前模块的路径(模块的 key 值维护的数组)以及是否设置了 namespced 计算出 namespce

const store = new Vuex.Store({
  modules: {
    a: moduleA, // path = ['a']
    b: {
      modules: {
        namespced: true
        c: moduleC  // path = ['b', 'c']
      }
    }
  }
})

getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      // 有命名空间的情况, 比如命名为 a, 最后的 namespace 为  a/ 
      // 最后有斜杠,因为还要追加 mutations actions 的 key 值
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }

在这个例子中,a、b 模块的 namespce 都为 '', c 模块的为 c/

然后使用 mutation action getter 的 key 拼接成一个新的 key 值作为最终的 namespcedType 存储起来。

这样会导致,a、b 模块中相同 key 值的 mutation action 会被一个 commitdispatch 触发。

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

我觉得这是一个坑点,大部分时候我们都会保持唯一的命名。所以设置模块的时候,应该尽量设置 namespced = true

用 store 创建一个 vue

function resetStoreVM (store, state, hot) {
  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure environment.
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }

}

在 vuex 中,我们的数据分为两种形式:

  1. state,通过 store.state 获取
  2. getter,通过 store.getter 获取

由于我们传递的模块 getter 是函数,提供的 API 是直接取值,因此需要根据 key 值定义 get 函数,同时将函数存到 computed 对象中,这样做使 getter 被使用的时候才会计算,是一个优化。

最后用 state computed 创建一个 vue。

为什么需要创建 vue

将 getter 函数转换为 vue 中的计算属性的好处已经说过了。同时很重要的一点是,store中的属性得是响应式属性。为什么呢?

我们可以通过 this.$store可以获取到数据,数据可能要在模板中使用,比如 <template><div>{{ $store.state.count }}</div></template>。因此必须要转换成响应式属性,才能触发模板更新。

其他问题

怎么保证只有 mutation 才能修改 state ?

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}	

function _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }

commit 函数会标记一个 flag,同时使用 watcher 监听 state 属性。如果发现,状态改变了,并且没有这个标记,那么就不是正常的修改,在非生产环境给出提示。

我们知道,vue 中 watcher 的执行时一般异步的,但如果需要拦截到这个状态,比如在fn执行结束之前触发 watcher。因此设置了 sync: true,这个选项意味着,一旦数据进行了set操作,watcher 会马上执行。

这个选项在 vue 文档中没有提到,毕竟 vue 使用 queue 的方式进行了优化,一般我们写代码也没有这么强烈的同步需求,可能会被滥用。

一些想法

vuex 的代码写的很精巧,设计的很好。自己平时写代码很少写这样的类设计。

原本以为 vuex 是通过 provide/reject实现的,后面查文档时发现

提示:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

vue 和它的响应式原理是密不可分的。只要对象是响应式的,就可以触发它的机制,那么也就不需要使用provide/inject这种方式。像 vuex 一样单独抽离出一个 vue 对象就可以了,这和 eventBus 有些类似。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.