最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 尤雨溪的5KB petite-vue源码解析

    正文概述 掘金(Peter谭老师)   2021-07-14   429

    写在开头

    • 近期尤雨溪发布了5kb的petite-vue,好奇的我,clone了他的源码,给大家解析一波。
    • 最近由于工作事情多,所以放缓了原创的脚步!大家谅解
    • 想看我往期手写源码+各种源码解析的可以关注我公众号看我的GitHub,基本上前端的框架源码都有解析过

    正式开始

    • petite-vue是只有5kb的vue,我们先找到仓库,克隆下来
    https://github.com/vuejs/petite-vue
    
    • 克隆下来后发现,用的是vite + petite-vue + 多页面形式启动的

    • 启动命令:

    git clone https://github.com/vuejs/petite-vue
    cd /petite-vue
    npm i 
    npm run dev
    
    
    • 然后打开http://localhost:3000/即可看到页面:

    尤雨溪的5KB petite-vue源码解析


    保姆式教学

    • 项目已经启动了,接下来我们先解析下项目入口,由于使用的构建工具是vite,从根目录下的index.html人口找起:
    <h2>Examples</h2>
    <ul>
      <li><a href="/examples/todomvc.html">TodoMVC</a></li>
      <li><a href="/examples/commits.html">Commits</a></li>
      <li><a href="/examples/grid.html">Grid</a></li>
      <li><a href="/examples/markdown.html">Markdown</a></li>
      <li><a href="/examples/svg.html">SVG</a></li>
      <li><a href="/examples/tree.html">Tree</a></li>
    </ul>
    
    <h2>Tests</h2>
    <ul>
      <li><a href="/tests/scope.html">v-scope</a></li>
      <li><a href="/tests/effect.html">v-effect</a></li>
      <li><a href="/tests/bind.html">v-bind</a></li>
      <li><a href="/tests/on.html">v-on</a></li>
      <li><a href="/tests/if.html">v-if</a></li>
      <li><a href="/tests/for.html">v-for</a></li>
      <li><a href="/tests/model.html">v-model</a></li>
      <li><a href="/tests/once.html">v-once</a></li>
      <li><a href="/tests/multi-mount.html">Multi mount</a></li>
    </ul>
    
    <style>
      a {
        font-size: 18px;
      }
    </style>
    
    • 这就是多页面模式+vue+vite的一个演示项目,我们找到一个简单的演示页commits:
    <script type="module">
      import { createApp, reactive } from '../src'
    
      const API_URL = `https://api.github.com/repos/vuejs/vue-next/commits?per_page=3&sha=`
    
      createApp({
        branches: ['master', 'v2-compat'],
        currentBranch: 'master',
        commits: null,
    
        truncate(v) {
          const newline = v.indexOf('\n')
          return newline > 0 ? v.slice(0, newline) : v
        },
    
        formatDate(v) {
          return v.replace(/T|Z/g, ' ')
        },
    
        fetchData() {
          fetch(`${API_URL}${this.currentBranch}`)
            .then((res) => res.json())
            .then((data) => {
              this.commits = data
            })
        }
      }).mount()
    </script>
    
    <div v-scope v-effect="fetchData()">
      <h1>Latest Vue.js Commits</h1>
      <template v-for="branch in branches">
        <input
          type="radio"
          :id="branch"
          :value="branch"
          name="branch"
          v-model="currentBranch"
        />
        <label :for="branch">{{ branch }}</label>
      </template>
      <p>vuejs/vue@{{ currentBranch }}</p>
      <ul>
        <li v-for="{ html_url, sha, author, commit } in commits">
          <a :href="html_url" target="_blank" class="commit"
            >{{ sha.slice(0, 7) }}</a
          >
          - <span class="message">{{ truncate(commit.message) }}</span><br />
          by
          <span class="author"
            ><a :href="author.html_url" target="_blank"
              >{{ commit.author.name }}</a
            ></span
          >
          at <span class="date">{{ formatDate(commit.author.date) }}</span>
        </li>
      </ul>
    </div>
    
    <style>
      body {
        font-family: 'Helvetica', Arial, sans-serif;
      }
      a {
        text-decoration: none;
        color: #f66;
      }
      li {
        line-height: 1.5em;
        margin-bottom: 20px;
      }
      .author, .date {
        font-weight: bold;
      }
    </style>
    
    
    • 可以看到页面顶部引入了
    import { createApp, reactive } from '../src'
    

    开始从源码启动函数入手

    • 启动函数为createApp,找到源码:
    //index.ts
    export { createApp } from './app'
    ...
    import { createApp } from './app'
    
    let s
    if ((s = document.currentScript) && s.hasAttribute('init')) {
      createApp().mount()
    }
    
    
    • 上面这段代码意思是,创建s变量记录当前运行的脚本元素,如果存在制定属性init,那么就调用createApp和mount方法.

    • 但是这里项目里面是主动调用了暴露的createApp方法,我们去看看createApp这个方法的源码,有大概80行代码

    import { reactive } from '@vue/reactivity'
    import { Block } from './block'
    import { Directive } from './directives'
    import { createContext } from './context'
    import { toDisplayString } from './directives/text'
    import { nextTick } from './scheduler'
    
    export default function createApp(initialData?: any){
    ...
    }
    
    
    • createApp方法接收一个初始数据,可以是任意类型,也可以不传。这个方法是入口函数,依赖的函数也比较多,我们要静下心来。这个函数进来就搞了一堆东西
    createApp(initialData?: any){
       // root context
      const ctx = createContext()
      if (initialData) {
        ctx.scope = reactive(initialData)
      }
    
      // global internal helpers
      ctx.scope.$s = toDisplayString
      ctx.scope.$nextTick = nextTick
      ctx.scope.$refs = Object.create(null)
    
      let rootBlocks: Block[]
    
    }
    
    • 上面这段代码,是创建了一个ctx上下文对象,并且给它上面赋予了很多属性和方法。然后提供给createApp返回的对象使用
    • createContext创建上下文:
    export const createContext = (parent?: Context): Context => {
      const ctx: Context = {
        ...parent,
        scope: parent ? parent.scope : reactive({}),
        dirs: parent ? parent.dirs : {},
        effects: [],
        blocks: [],
        cleanups: [],
        effect: (fn) => {
          if (inOnce) {
            queueJob(fn)
            return fn as any
          }
          const e: ReactiveEffect = rawEffect(fn, {
            scheduler: () => queueJob(e)
          })
          ctx.effects.push(e)
          return e
        }
      }
      return ctx
    }
    
    
    • 根据传入的父对象,做一个简单的继承,然后返回一个新的ctx对象。
    return {
      directive(name: string, def?: Directive) {
          if (def) {
            ctx.dirs[name] = def
            return this
          } else {
            return ctx.dirs[name]
          }
        },
    mount(el?: string | Element | null){}...,
    unmount(){}...
    }
    
    
    • 对象上有三个方法,例如directive指令就会用到ctx的属性和方法。所以上面一开始搞一大堆东西挂载到ctx上,是为了给下面的方法使用

    • 重点看mount方法:

         mount(el?: string | Element | null) {
         if (typeof el === 'string') {
            el = document.querySelector(el)
            if (!el) {
              import.meta.env.DEV &&
                console.error(`selector ${el} has no matching element.`)
              return
            }
          }
         ...
        
         }
    
    • 首先会判断如果传入的是string,那么就回去找这个节点,否则就会找document
    el = el || document.documentElement
    
    • 定义roots,一个节点数组
    let roots: Element[]
         if (el.hasAttribute('v-scope')) {
           roots = [el]
         } else {
           roots = [...el.querySelectorAll(`[v-scope]`)].filter(
             (root) => !root.matches(`[v-scope] [v-scope]`)
           )
         }
         if (!roots.length) {
           roots = [el]
         }
    
    • 如果有v-scope这个属性,就把el存入数组中,赋值给roots,否则就要去这个el下面找到所以的带v-scope属性的节点,然后筛选出这些带v-scope属性下面的不带v-scope属性的节点,塞入roots数组
    • 在把roots处理完毕后,开始行动。
      rootBlocks = roots.map((el) => new Block(el, ctx, true))
          // remove all v-cloak after mount
          ;[el, ...el.querySelectorAll(`[v-cloak]`)].forEach((el) =>
            el.removeAttribute('v-cloak')
          )
    
    • 这个Block构造函数是重点,将节点和上下文传入以后,外面就只是去除掉'v-cloak'属性,这个mount函数就调用结束了,那么怎么原理就隐藏在Block里面。
    • Block原来不是一个函数,而是一个class.

    尤雨溪的5KB petite-vue源码解析

    • 在constructor构造函数中可以看到
      constructor(template: Element, parentCtx: Context, isRoot = false) {
        this.isFragment = template instanceof HTMLTemplateElement
    
        if (isRoot) {
          this.template = template
        } else if (this.isFragment) {
          this.template = (template as HTMLTemplateElement).content.cloneNode(
            true
          ) as DocumentFragment
        } else {
          this.template = template.cloneNode(true) as Element
        }
    
        if (isRoot) {
          this.ctx = parentCtx
        } else {
          // create child context
          this.parentCtx = parentCtx
          parentCtx.blocks.push(this)
          this.ctx = createContext(parentCtx)
        }
    
        walk(this.template, this.ctx)
      }
    
    • 以上代码可以分为三个逻辑
      • 创建模板template(使用clone节点的方式,由于dom节点获取到以后是一个对象,所以做了一层clone)
      • 如果不是根节点就递归式的继承ctx上下文
      • 在处理完ctx和Template后,调用walk函数
    • walk函数解析:

    尤雨溪的5KB petite-vue源码解析

    • 会先根据nodetype进行判断,然后做不同的处理

    • 如果是一个element节点,就要处理不同的指令,例如v-if

    尤雨溪的5KB petite-vue源码解析

    • 这里有一个工具函数要先看看
    export const checkAttr = (el: Element, name: string): string | null => {
      const val = el.getAttribute(name)
      if (val != null) el.removeAttribute(name)
      return val
    }
    
    • 这个函数意思是检测下这个节点是否包含v-xx的属性,然后返回这个结果并且删除这个属性

    • v-if举例,当判断这个节点有v-if属性后,那么就去调用方法处理它,并且删除掉这个属性(作为标识已经处理过了)

    • v-if处理函数大概60行
    export const _if = (el: Element, exp: string, ctx: Context) => {
    ...
    }
    
    • 首先_if函数先拿到el节点和exp这个v-if的值,以及ctx上下文对象
      if (import.meta.env.DEV && !exp.trim()) {
        console.warn(`v-if expression cannot be empty.`)
      }
    
    
    • 如果为空的话报出警告
    • 然后拿到el节点的父节点,并且根据这个exp的值创建一个comment注释节点(暂存)并且插入到el之前,同时创建一个branches数组,储存exp和el
     const parent = el.parentElement!
      const anchor = new Comment('v-if')
      parent.insertBefore(anchor, el)
    
      const branches: Branch[] = [
        {
          exp,
          el
        }
      ]
    
      // locate else branch
      let elseEl: Element | null
      let elseExp: string | null
    
    • 接着创建elseElelseExp的变量,并且循环遍历搜集了所有的else分支,并且存储在了branches里面
      while ((elseEl = el.nextElementSibling)) {
        elseExp = null
        if (
          checkAttr(elseEl, 'v-else') === '' ||
          (elseExp = checkAttr(elseEl, 'v-else-if'))
        ) {
          parent.removeChild(elseEl)
          branches.push({ exp: elseExp, el: elseEl })
        } else {
          break
        }
      }
    
    • 接下来根据副作用函数的触发,每次都去branches里面遍历寻找到需要激活的那个分支,将节点插入到parentNode中,并且返回nextNode即可实现v-if的效果

    尤雨溪的5KB petite-vue源码解析

     // process children first before self attrs
        walkChildren(el, ctx)
    
    
    const walkChildren = (node: Element | DocumentFragment, ctx: Context) => {
      let child = node.firstChild
      while (child) {
        child = walk(child, ctx) || child.nextSibling
      }
    }
    
    
    • 当节点上没有v-if之类的属性时,这个时候就去取他们的第一个子节点去做上述的动作,匹配每个v-if v-for之类的指令
    如果是文本节点
    else if (type === 3) {
        // Text
        const data = (node as Text).data
        if (data.includes('{{')) {
          let segments: string[] = []
          let lastIndex = 0
          let match
          while ((match = interpolationRE.exec(data))) {
            const leading = data.slice(lastIndex, match.index)
            if (leading) segments.push(JSON.stringify(leading))
            segments.push(`$s(${match[1]})`)
            lastIndex = match.index + match[0].length
          }
          if (lastIndex < data.length) {
            segments.push(JSON.stringify(data.slice(lastIndex)))
          }
          applyDirective(node, text, segments.join('+'), ctx)
        }
    
    • applyDirective函数
    const applyDirective = (
      el: Node,
      dir: Directive<any>,
      exp: string,
      ctx: Context,
      arg?: string,
      modifiers?: Record<string, true>
    ) => {
      const get = (e = exp) => evaluate(ctx.scope, e, el)
      const cleanup = dir({
        el,
        get,
        effect: ctx.effect,
        ctx,
        exp,
        arg,
        modifiers
      })
      if (cleanup) {
        ctx.cleanups.push(cleanup)
      }
    }
    
    • 接下来nodeType是11意味着是一个Fragment节点,那么直接从它的第一个子节点开始即可
    } else if (type === 11) {
        walkChildren(node as DocumentFragment, ctx)
      }
    
    nodeType 说 明
    此属性只读且传回一个数值。
    有效的数值符合以下的型别:
    1-ELEMENT
    2-ATTRIBUTE
    3-TEXT
    4-CDATA
    5-ENTITY REFERENCE
    6-ENTITY
    7-PI (processing instruction)
    8-COMMENT
    9-DOCUMENT
    10-DOCUMENT TYPE
    11-DOCUMENT FRAGMENT
    12-NOTATION
    

    梳理总结

    • 拉取代码
    • 启动项目
    • 找到入口createApp函数
    • 定义ctx以及层层继承
    • 发现block方法
    • 根据节点是element还是text分开做处理
    • 如果是text就去通过正则匹配,拿到数据返回字符串
    • 如果是element就去做一个递归处理,解析所有的v-if等模板语法,返回真实的节点

    有趣的源码补充

    • 这里的nextTick实现,是直接通过promise.then
    const p = Promise.resolve()
    
    export const nextTick = (fn: () => void) => p.then(fn)
    
    

    写在最后

    • 有点晚了,写到1点多不知不觉,如果感觉写得不错,帮我点波再看/关注/赞吧
    • 如果你想看往期的源码分析文章可以关注我的gitHub - 公众号:前端巅峰

    起源地下载网 » 尤雨溪的5KB petite-vue源码解析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元