创意电子

标题: (建议收藏)第一人称视角带你走进 Vue 源码世界 [打印本页]

作者: 前端人    时间: 2021-9-11 00:30
标题: (建议收藏)第一人称视角带你走进 Vue 源码世界
本文不引战,成熟的人应该脱离框架的范畴,而不是纠结谁更好或者谁更不好。有道是黑猫白猫,抓到老鼠就是好猫。
所以本文会带大家读源码。简单易懂,大佬小白都能看明白。并劳绩益处。
从 new 一个 Vue的实例粗来开始

准备工作: chrome 打开 Vue github 地点
> cd 「你的路径」> git clone https://github.com/vuejs/vue.git> code vue (ps: 此命令为用VS code 打开 vue 项目)复制代码看一下vue 项目标目次结构:

                               
登录/注册后可看大图

哇,结构很清晰噢! 似乎莫名的木有那么复杂嘛(为分析源码做心理建设)
全局安装 serve:
> npm i -g serveOr> yarn global add serve> serve .复制代码So you shoud open in: localhost:5000

                               
登录/注册后可看大图

咦~ 项目标目次结构就在浏览器可视化了。
点击 examples ,再找到 markdown, 此时你的url是 http://localhost:5000/examples/markdown/
哇哦,可以玩了。例如:

                               
登录/注册后可看大图

咦~ 左边写markdown 语法,右边就实时展示粗来了?No,这个不重要。重要的是,代码!
          Vue.js markdown editor example                                       
   
      复制代码细看我们的 script 标签, new Vue({...}) 是不是开始有点东西了?

       ...                          Show Modal                            custom header

         
      复制代码这个栗子的功能很简单,就是页面上一个按钮,点击按钮会弹出一个框。你大概会觉得有点无聊,不要紧,立刻开始搞事情。

                               
登录/注册后可看大图

现在,你把 template for modal 那个script里的标签删掉,在点击页面上那个按钮。

                               
登录/注册后可看大图

页面上即没有框弹出来,控制台也木有提示任何报错信息。 同样,你干掉 注册全局Vue组件,得到的结果是 页面内容有变革,但依然木有出现弹框。

                               
登录/注册后可看大图

那么,只根据以上的尝试,总结以下几点:
废话不多说,加几行代码看看。
const app =  new Vue({        el: '#app',        data: {          showModal: false        }      })console.log('Vue.component:',Vue.component)console.log('Vue:',Vue, 'new Vue:', app)复制代码看图:

                               
登录/注册后可看大图

哇! 打印出来的结果令人欣喜!你看到了什么?? 咱就抛开以往的知识,你觉得你认识哪几个单词?
先看 Vue.component 的打印结果。
再看 new Vue({...}) 的打印结果.
下一步: 打个断点!如图

                               
登录/注册后可看大图


                               
登录/注册后可看大图

然后页面刷新,代码停在了。 你会发现代码会经过 emptyObject、Vnode、Observer、Watcher处(因为我打好断点了,你可以根据上图自行找到并打好断点)。
最终,你会到这里。

                               
登录/注册后可看大图

有木有觉得这段代码和上面那里看到的很像? 对! 就是console 打印出来的 Vue.component .
破案了!终于找到看源码的入口了!!
岑寂一下,先想想我们怎样顺利的进入源码世界

你写Vue,你会看到哪些东西?咱就从最简单的(所见即所得)开始。
模板编译

template 转换为 渲染函数,也就是我们常说的render 。(React 当中也有 render 函数的概念,但它们不是一个东西,有一样也有区别)
// 此为 我们在  template 中 利用(写)的 HTML标签语法内容,Show Modal复制代码那,其对应的render函数是什么?
render(h){    return h(    'button',    {        on: {            click: ()=> this.showModal = true        }    },    Show Modal    )}复制代码啊 哈? 你问我 那个 h , 那个 on 是什么东西? 哎呦,我似乎一下子没刹住,车开远了。没事,背面会解释(假造 DOM) 。至于怎样证明模板编译最终是个render,实在你在webpack打包后的文件里稍微找一下便一目了然了。 我们的.vue 文件就会被转化成render函数(通过 vue-loader)。
先来个小证明:render函数 到底是个什么东西。 加行代码:
      // register modal component      Vue.component('modal', {        template: '#modal-template'      })      // start app     const vm =  new Vue({        el: '#app',        data: {          showModal: false        }      })     console.log('render:',vm.$options.render)复制代码这句console 打印出了什么? 如图:
那么,这件事情是在那里处置惩罚的? 源码位置:/vue-dev/src/platforms/web/entry-runtime-with-compiler.js
ps: 只截取部分源码代码, 关键位置我用中文注释了。
忽略代码: code ...
// 只截取部分源码 Vue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  el = el && query(el) // 获取传入的el对象  /* istanbul ignore if */   /* el不可以是 body 和 html对象 */  if (el === document.body || el === document.documentElement) {    process.env.NODE_ENV !== 'production' && warn(      `Do not mount Vue to  or  - mount to normal elements instead.`    )    return this  }  const options = this.$options  // resolve template/el and convert to render function  // 将template/el 转成 render 函数。render挂在this.options 身上  if (!options.render) { // 如果没有render属性,则 想办法 搞一个 render 函数    let template = options.template    if (template) {       if (typeof template === 'string') {        // code ...        template = idToTemplate(template) //该方法返回 el 的innerHTML        // code ...      } else if (template.nodeType) { // nodeType 是代表 template 和 el 是一样的。        template = template.innerHTML // 如果模板是element ,拿template的 innerHTML      } else {        if (process.env.NODE_ENV !== 'production') { // 啥也不是,非法          warn('invalid template option:' + template, this)        }        return this      }    } else if (el) {      template = getOuterHTML(el); // 没有template,就用el的 outerHTML 作为模板    }    if (template) { // 好的,经过以上处置惩罚,这时间肯定有模板了!      /* istanbul ignore if */      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {        mark('compile')      }      // 敲黑板,划重点!! 此处 compileToFunctions 把 template 转换成 render 函数      const { render, staticRenderFns } = compileToFunctions(template, {        // code ...      }, this)      options.render = render      options.staticRenderFns = staticRenderFns      /* istanbul ignore if */      //code...    }  }  // 最终,要渲染DOM了。   return mount.call(this, el, hydrating)}复制代码稍微小结一下(new Vue({...}) 到底干了什么):
咦~ 太啰嗦了。 总结成一句话就是: 要保证Vue.$options.render方法是存在的。因为要计算VDOM。
回到开始,那render函数打印出来到底是个什么东西?如图:

                               
登录/注册后可看大图

我把代码format一下:
  (function anonymous(  ) {    with (this) {      return _c(        'div',        { attrs: { "id": "app" } },        [_c(          'button',          {            attrs: { "id": "show-modal" },            on: { "click": function ($event) { showModal = true } }          },          [_v("Show Modal")]        ),        _v(" "),        (showModal)          ?          _c('modal',            {              on: { "close": function ($event) { showModal = false } }            },            [              _c('h3',                { attrs: { "slot": "header" }, slot: "header" },                [_v("custom header")]              )]          )          :          _e()],        1)    }  })复制代码看得出,转化后的render函数实在就是个匿名函数(不是闭包)。 其中 _c 方法实在就是 上述 所说的 h(啪!这是个误会,很容易被人误会。 _c 就是 h ?)。
这里还有个小点,就是为什么会有 showModal ? _c : _e ? 它代表什么? 哈哈,我把模板代码写出来看看。
          Show Modal                            custom header

         
复制代码对,你猜的没错。 v-if="showModal" 就是 showModal ? _c : _e , 而且是DOM 有和无的差异。
ps: 有人想问 with (this) 是个什么意思了。 它的意思是,在with的作用域内,this为该域的最高级别的对象。 也就是在with的作用域内的“window”, 访问 _c、_e等 会默认找至 with。
然而,根据常识,一般 _x 这种单字母格式的方法一般都在 core 目次下,Vue 也不例外。
so, 有须要找到定义 _c 的方法的位置。 上源码:
export function initRender (vm: Component) {  vm._vnode = null // the root of the child tree  vm._staticTrees = null // v-once cached trees  const options = vm.$options  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree  const renderContext = parentVnode && parentVnode.context  vm.$slots = resolveSlots(options._renderChildren, renderContext)  vm.$scopedSlots = emptyObject  // bind the createElement fn to this instance  // so that we get proper render context inside it.  // args order: tag, data, children, normalizationType, alwaysNormalize  // internal version is used by render functions compiled from templates  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // 1  // normalization is always applied for the public version, used in  // user-written render functions.  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) // 2  // $attrs & $listeners are exposed for easier HOC creation.  // they need to be reactive so that HOCs using them are always updated  const parentData = parentVnode && parentVnode.data  /* istanbul ignore else */  if (process.env.NODE_ENV !== 'production') {    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)    }, true)    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm) // 3    }, true)  } else {    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)  }}复制代码请仔细看三个点, 代码中为加了数字标识。
小结一下:
再看下 /src/core/render-helpers/index.js。 这个看了之后,估计就能清晰很多了。
/* @flow *///code ...import ...//code ...export function installRenderHelpers (target: any) {  target._o = markOnce  target._n = toNumber  target._s = toString  target._l = renderList  target._t = renderSlot  target._q = looseEqual  target._i = looseIndexOf  target._m = renderStatic  target._f = resolveFilter  target._k = checkKeyCodes  target._b = bindObjectProps  target._v = createTextVNode  target._e = createEmptyVNode  target._u = resolveScopedSlots  target._g = bindObjectListeners  target._d = bindDynamicKeys // 1  target._p = prependModifier}复制代码看,是不是清晰了很多。原来那么多_开头的函数都是在这被赋予的。 其中看下 bindDynamicKeys, 是的,你想得没错,你用 v-for 的时绑定的key 就是它完成的!(此处求源码打脸,我就不翻了……)
emmm, 似乎前面的栗子还出现了 _v 方法,照旧得解释一下 createTextVNode 方法。
path: src/core/vdom/vnode.js
// code...export const createEmptyVNode = (text: string = '') => {  const node = new VNode()  node.text = text  node.isComment = true  return node}export function createTextVNode (val: string | number) {  return new VNode(undefined, undefined, undefined, String(val))}// code...复制代码唉,这没啥好说的了。 就是有个 VNode类,可以new 出 空的vnode(假造节点)和 文本类型的vnode。至于vnode可以访问哪些属性和方法,你可以继续追根溯源……
这里,为了让大家能更深刻的明白html -> render 的过程,给个好玩的地点:template-explorer
Q: 我们知道 template 最终也是要转化成 js 的,不然浏览器咋识别? 那 template是怎样转化成 最终的js的? 答:template -> AST -> 优化后的AST -> render
好吧,继续上源码。 path: /src/compiler/index.js
/* @flow */import { parse } from './parser/index'import { optimize } from './optimizer'import { generate } from './codegen/index'import { createCompilerCreator } from './create-compiler'// `createCompilerCreator` allows creating compilers that use alternative// parser/optimizer/codegen, e.g the SSR optimizing compiler.// Here we just export a default compiler using the default parts.export const createCompiler = createCompilerCreator(function baseCompile (  template: string,  options: CompilerOptions): CompiledResult {  const ast = parse(template.trim(), options) // template -> AST  if (options.optimize !== false) {    optimize(ast, options) // AST -> 优化后的AST  }  const code = generate(ast, options) // 优化后的AST -> code.render  return {    ast,    render: code.render, // 这个render 是 string, 利用时需要转化成function    staticRenderFns: code.staticRenderFns // 静态渲染函数,能得到一颗静态的VNode树  }})复制代码这段源码不多,关键位置有中午注释,一看即懂。
Vue 的 组件化到底是个啥

官方说法是: Vue 组件 就是一个拥有预定义属性的 Vue 实例。 说人话就是: new Vue({...})
那一个Vue组件包含哪些东西? 很明了了……
那么,Vue 中 可以注册 全局组件局部组件 。 怎样做的呢? 再来回顾一下上面的一个栗子:
  // register modal component 。modal 就是全局组件      Vue.component('modal', {        template: '#modal-template'      })      // start app     const vm =  new Vue({        el: '#app',        data: {          showModal: false        }      })复制代码很明显,通过 Vue.component 注册全局组件。 上源码,path:/src/core/global-api/index.js
// code ...export function initGlobalAPI (Vue: GlobalAPI) { // code ...  Object.defineProperty(Vue, 'config', configDef) // 响应式……的开始?// code ...  // this is used to identify the "base" constructor to extend all plain-object  // components with in Weex's multi-instance scenarios.  Vue.options._base = Vue // 把Vue 的构造函数赋给 Vue.options._base  extend(Vue.options.components, builtInComponents) // keep-alive setting  initUse(Vue)  initMixin(Vue)  initExtend(Vue)  initAssetRegisters(Vue)}复制代码有个单词组很敏感,initAssetRegisters , 啥? 初始化什么东西?上源码
// path: /src/shared/constants.jsexport const ASSET_TYPES = [  'component',  'directive',  'filter']// path:/src/core/global-api/assets.jsexport function initAssetRegisters (Vue: GlobalAPI) {  /**   * Create asset registration methods.   */  ASSET_TYPES.forEach(type => {    Vue[type] = function (      id: string,      definition: Function | Object // 这里能看出啥??    ): Function | Object | void {      if (!definition) { // 2. 如果没传 definition ,说明可以直接获取先前已经定义好的全局组件        return this.options[type + 's'][id]      } else {         /* istanbul ignore if */        if (process.env.NODE_ENV !== 'production' && type === 'component') {          validateComponentName(id)        }        if (type === 'component' && isPlainObject(definition)) {          definition.name = definition.name || id          definition = this.options._base.extend(definition) // 3. 把组件的配置选项转化成组件的构造函数        }        if (type === 'directive' && typeof definition === 'function') {          definition = { bind: definition, update: definition }        }        this.options[type + 's'][id] = definition //最终进行全局注册        return definition      }    }  })}复制代码直接小结一下:
Q: definition: Function | Object // 这里能看出啥?? 这行代码有什么用?
答:据说 Vue 也可以写 jsx。 道理在这,因为jsx就是个 fuction
Q:为什么说每个Vue组件是Vue实例呢?
答:因为Vue组件继承了Vue。 path: /src/core/global-api/extend.js
/* @flow */import ...export function initExtend (Vue: GlobalAPI) {  //code ...  /**   * Class inheritance   */  Vue.extend = function (extendOptions: Object): Function {    extendOptions = extendOptions || {}    const Super = this    // code...    const Sub = function VueComponent (options) {      this._init(options)    }    // 此处Sub类继承了Super,Super 是this ,this指向Vue。 所以Sub的实例能访问Vue的属性    Sub.prototype = Object.create(Super.prototype)    Sub.prototype.constructor = Sub      // code...      return Sub  }}
作者:在剥我的壳
链接:https://juejin.cn/post/7005956935937687583
来源:掘金
作者: terrycc    时间: 2021-9-11 08:19
转发了
作者: 重庆最帅的锅    时间: 2021-9-11 22:13
转发了
作者: 摸着鹰酱过河    时间: 2021-9-12 10:39
转发了
作者: 竹根九    时间: 2021-9-12 11:40
转发了
作者: 爆发小宇宙X1    时间: 2021-9-12 15:17
学习了
作者: 不羁如风    时间: 2021-9-13 17:26
不错,非常棒,通俗易懂,接地气,更大白话一点更好,专业的术语用浅显的语言解释一下更好,继续加油




欢迎光临 创意电子 (https://wxcydz.cc/) Powered by Discuz! X3.4