09 | 【阅读Vue2源码】浅析Template生成DOM的过程

398 阅读9分钟

前言

接着上篇文章的内容(08 | 【阅读Vue2源码】Template被Vue编译成了什么?),由于篇幅有限,并且实现过程也确实挺复杂的,所以分两篇文章来记录。

上篇文章已经研究了Template转成render函数的过程,那么,本篇文章就接着研究render函数是如何生成标准的DOM,并且Vue是如何更新视图的。

在了解了实现原理之后,尝试自己动手实现。

在阅读源码和自己动手实现的过程中,发现这个过程确实很难很痛苦

  1. 要写好文章也很难,要记录自己对源码理解、也要关注写的文章其他人能否读懂,真的挺难。挺耗时间的。
  2. 但是又总会想到雷总的一句话,“你所经历的所有挫折、失败,甚至那些看似毫无意义消磨时间的事情,都将成为你最宝贵的财富。”继续坚持,砥砺前行。

示例Demo代码

沿用上篇文章(08 | 【阅读Vue2源码】Template被Vue编译成了什么?)中的demo示例代码继续分析

<section id="app">
  <button @click="plus">+1</button>
  <div class="count-cls" :class="['count-text']">count:{{ count }}</div>
  <div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count }}</div>
  </section>

const app = new Vue({
  data() {
    return {
      count: 0
    }
  },
  methods: {
    plus() {
      this.count += 1;
    }
  }
})

app.$mount('#app')

借用上篇文章中生成的render函数

function render() {
  return with(this) {_c('section', {"attrs":{"id":"app"}}, [_c('button', {"on":{"click":{"value":"plus"}},"attrs":{}}, [_v('+1')]), _c('div', {"attrs":{"class":"count-cls count-text"}}, [_v('count:' + _s('count'))]), count % 2 === 0 ? _c('div', {"attrs":{}}, [_v('doubleCount:' + _s('count'))]) : _e()])}
}

源码分析

思维导图

简单回顾template生成render函数的过程

  1. Vue组件初始化完,调用$mount()挂载元素
  2. $mount获取template字符串,底层调用baseCompile函数处理
  3. baseCompile的逻辑为:调用parse解析template生成ast
  4. 调用genrate函数根据ast生成render函数的函数体字符串
  5. 使用new Function,以前面步骤构建的函数体字符串作为参数生成render函数
  6. 生成完render函数,回到$mount的函数体,继续调用mount函数
  7. 接下来,本文将继续分析mount函数之后的逻辑

生成render函数之后

执行mount

在生成render函数之后,回到$mount的函数体,继续调用mount函数

mount函数是之前的$mount,当前这个入口文件为entry-runtime-with-compiler.js,带编译器的入口文件,需要增加一些额外的逻辑,所以把原来的$mount函数缓存了一份为mount,执行完额外的逻辑后再执行原来的$mount,即mount

// src/platforms/web/entry-runtime-with-compiler.js
// 先缓存一份原来的$mount方法
const mount = Vue.prototype.$mount
// 重新赋值$mount方法,主要是要加入编译函数compileToFunctions生成render方法
Vue.prototype.$mount = function () {
  // ...
  return mount.call(this, el, hydrating)
}

继续debugger,进入mount的函数体

// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

可以看到,这里主要是调用mountComponent,进入mountComponent看看做了什么

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // ...
  callHook(vm, 'beforeMount')

  let updateComponent // 声明updateComponent,等待赋值
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
    	// ...
      const vnode = vm._render()
      vm._update(vnode, hydrating)
      // ...
    }
  } else {
    // 这里是核心逻辑,把vm._render()执行结果VNode作为参数传递给_update执行
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // 这里是最核心的逻辑:把updateComponent更新函数放到Watcher的回调中进行监听,如果vm的数据有更新,则执行updateComponent函数,更新视图。
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

mountComponent的逻辑也很清晰,主要逻辑为

  1. 声明updateComponent,等待赋值一个函数
  2. updateComponent的函数体为vm._update(vm._render(), hydrating)
  • 执行vm._render()函数得到vnode
  • 执行vm._update更新视图
  1. new Watcher作为渲染的watcher,以updateComponent作为回调函数,当vm发生变化时,执行updateComponent,更新视图

到这里,可以了解到Vue更新视图的大概的流程和原理了。

触发updateComponent

在之前分析watch的实现原理的文章(05 | 【阅读Vue2源码】watch实现原理)中,我们知道了,new Watcher时(非computedWatcher)会先执行getter函数取一下值,也就会执行cb,这里的cb就是updateComponent

也就是说,new Watcher时,里面会执行updateComponent,然后进入updateComponent的逻辑

再跟着debugger,进入updateComponent

_update执行更新

里面执行vm._update,它接收的第一个参数为_render()的返回值,那么我们看看_render函数

// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  // ...
  let vnode
  try {
    // 核心逻辑,执行render,得到vnode
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    // ...
  } finally {
    // ...
  }
  // ...
  return vnode
}

核心逻辑,就是调用之前生成的render函数,生成vnode,再返回vnode,简单来说就是包装了一下render函数,就成了现在_render函数

生成VNode

从代码中可以看到,执行render函数生成的VNoderender函数接收两个参数,第一个是vue实例,用于绑定作用域,第二个参数是$createElement,用于创建VNode

$createElement的函数是在Vue初始化的创建的,调用链路为new Vue() -> this._init() -> initRender()

// src/core/instance/render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 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)

可以看到vm.$createElementvm._c都是createElement,支持最后一个参数不同,那么看看createElement的实现。

createElement

createElement的源码位置在:src/core/vdom/create-element.js

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // ...
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    return createEmptyVNode()
  }
  // ...
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 核心逻辑:new VNode创建VNode 和创建组件
    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
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 其他
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
	// 返回VNode
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

createElement内部调用_createElement,看看_createElement的实现,

核心逻辑为:

  1. 判断标签名是否是标准的html标签名,如果是直接new VNode
  2. 如果标签名是自定义的组件,则调用createComponent
  3. 其他标签名,也是new VNode
  4. 返回vnode实例

createElement这个函数并没有创建元素,只是返回了vnode,但是作者为什么要取这个函数名呢?我觉得还不如取createVNode呢,哈哈,这只是我自己的一个臆想。

那么VNode是什么样子的呢,看看VNode源码的定义

VNode

其实也很简单,就是一堆属性

// src/core/vdom/vnode.js
export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

vnode实例对象就是这个样子的

其中主要关注tagdatachildrenelmtextcontext、后面要根据这些属性来生成真实的DOM,赋值给elm属性保存着。

准备更新视图

执行完_render(),得到vnode作为参数传给_update,那么看看_update做了啥呢。

_update源码

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) { // 核心逻辑
    // initial render 初始化时走这个
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates 更新视图时走这个
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

这里的核心逻辑主要是执行vm.__patch__

分两种情况

  • 初始化时:vm.__patch__(vm.$el, vnode, hydrating, false),以$el作为第一个参数
  • 更新时:vm.__patch__(prevVnode, vnode),以prevVnode作为第一个参数

那么__patch__又是什么呢?它就是patch函数

// src/platforms/web/runtime/index.js
import { patch } from './patch'

// ...

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

实现视图的更新

看看patch的实现

// src/core/vdom/patch.js
export function createPatchFunction (backend) {

  // ...
  
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 初始化时是真正的元素,$el
        if (isRealElement) {
          // ...
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // 核心逻辑1,创建真实的DOM,挂载到vnode.elm中
        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

      	// ...

        // 核心逻辑2,移除旧元素
        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

这里最核心的逻辑就是

  1. createElm,创建真实的DOM,挂载到vnode.elm中,当新元素挂上去时,会出现新元素与旧元素并存的时刻(后面会移除旧元素)

  1. 移除旧元素,removeVnodes([oldVnode], 0, 0)
  2. 完成视图的更新,更新完成后就只剩最新的元素了

createElm

看看createElm具体实现

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    // ...

    // 核心逻辑:根据vnode中的tag,调用document.createElement创建真实的元素,挂载到vnode.elm
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    // 设置scope,即style的scoped的实现
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      // 创建子元素
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      // 将创建的元素挂载到父元素下面
      insert(parentElm, vnode.elm, refElm)
    }

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

这里的核心逻辑:

  1. 根据vnode中的tag,调用document.createElement创建真实的元素,挂载到vnode.elm
  2. 创建子元素,调用createChildren
  3. 将创建的元素挂载到父元素下面,insert(parentElm, vnode.elm, refElm)
  4. 接着回到patch函数

patch函数中移除旧节点,完成视图更新,这里已经完成了视图的首次更新,接着代码回调new Watcher之后的代码执行,我们要分析的内容也就完成了。

思维导图

其实看代码还是很枯燥的,可能看得迷迷糊糊,这里我画一张图,描述主要的过程

自己实现render转化DOM的过程

编写大致框架代码

<section id="app">
  <button @click="plus">+1</button>
  <div class="count-cls" :class="['count-text']">count:{{ count }}</div>
  <div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count * 2 }}</div>
</section>
const renderHelpers = miniRender();
const { MiniVue } = createMiniVue(renderHelpers);

const app = new MiniVue({
  el: document.getElementById("app"),
  data: {
    count: 0
  },
  methods: {
    plus() {
      this.count += 1;
    }
  }
})
app.$mount('#app');

创建MiniVue

因为我这个示例是在html的script标签中编写的,为了方便阅读和组织代码结构,我把MiniVue的实现代码都放到一个createMiniVue函数中,MiniVue的代码借用我之前的文章(05 | 【阅读Vue2源码】watch实现原理)中的代码。

做了一点改造:

  1. 挂载mountupdateComponent,因为需要挂载mount方法和updateComponent方法,所以通过参数的方式传递进来。
  2. vm增加_renderWatcher属性,用于保存渲染用的watcher
  3. MiniWatcherrun方法增加_renderWatchercb执行,用于更新视图
function createMiniVue(config) {
  const {mount, updateComponent} = config;
  MiniVue.prototype.$mount = mount;

  class MiniWatcher {
    vm = null; // 当前vue/vue组件实例
    cb = () => { }; // 回调函数
    getter = () => { }; // 取值函数
    expression = ''; // watch的键名
    user = false; // 是否是用户定义的watch
    value; // 当前观察的属性的值

    constructor(vm, expOrFn, cb, options = {}) {
      this.vm = vm;
      this.cb = cb;
      this.expression = expOrFn;
      // 对getter做区分处理
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
      } else {
        this.getter = parseExpression(this.expression, vm, this);
      }
      this.user = options.user;
      // 初始化lazy
      this.lazy = !!options.lazy;
      // 增加对computed的处理
      this.value = this.lazy ? undefined : this.get();
    }

    get() {
      const value = this.getter.call(this.vm);
      return value;
    }

    update() {
      nextTick(() => {
        this.run();
      })
    }

    run() {
      // 获取新值和旧值
      const newValue = this.get();
      const oldValue = this.value;
      this.value = newValue;
      this.cb.call(this.vm, newValue, oldValue);
      if (this.vm._renderWatcher) {
        this.vm._renderWatcher.cb();
      }
    }

    // 新增computed用的计算值的函数
    evaluate() {
      this.value = this.get();
    }
  }

  class MiniDep {
    static target = null;
    subs = [];

    depend(sub) {
      if (sub && !this.subs.includes(sub)) {
        this.subs.push(sub);
      }
    }

    notify() {
      this.subs.forEach(sub => {
        sub && sub.update();
      })
      MiniDep.target = null;
    }
  }

  // 解析表达式,返回一个函数
  function parseExpression(key, vm, watcher) {
    return () => {
      MiniDep.target = watcher;
      // 取值,触发getter,取值前先把watcher实例放到target中
      const value = vm.data[key];
      // 取完值后,清空Dep.target
      MiniDep.target = null;
      return value;
    }
  }

  function nextTick(cb) {
    return Promise.resolve().then(cb);
  }

  function MiniVue(options = {}) {
    const vm = this;
    this.$el = options.el;
    this.vm = this;
    this.data = options.data;
    this.watch = options.watch;
    this.deps = new Set();

    initData(vm, this.data); // 初始化data
    initWatch(this.watch); // 初始化watch
    initMethods(options.methods); // 初始化methods

    function observe(data) {
      for (const key in data) {
        defineReactive(data, key);
      }
    }

    function defineReactive(data, key) {
      const dep = new MiniDep();
      vm.deps.add(dep);
      const clonedData = JSON.parse(JSON.stringify(data));
      Object.defineProperty(data, key, {
        get: function reactiveGetter() {
          dep.depend(MiniDep.target || vm._renderWatcher);
          return clonedData[key];
        },
        set: function reactiveSetter(value) {
          dep.notify();
          clonedData[key] = value;
          return value;
        }
      });
    }

    function initData(vm, data = {}) {
      for (const key in data) {
        Object.defineProperty(vm, key, {
          configurable: true,
          enumerable: true,
          get() {
            return vm['data'][key];
          },
          set(val) {
            vm['data'][key] = val;
          }
        })
        observe(vm.data);
      }
    }

    function initWatch(watch = {}) {
      for (const key in watch) {
        new MiniWatcher(vm, key, watch[key], { user: true }); // user = true,标记这是用户定义的watch
      }
    }

    // 把methods的属性平铺到vm中
    function initMethods(methods = {}) {
      for (const key in methods) {
        if (Object.hasOwnProperty.call(methods, key)) {
          const method = methods[key];
          Object.defineProperty(vm, key, {
            value: method.bind(vm)
          })
        }
      }
    }

    vm._renderWatcher = new MiniWatcher(this, () => { }, () => {
      updateComponent(vm, vm.$el)
    })
  }

  return {
    MiniVue
  };
}

创建miniCompiler

miniCompiler是上篇文章(08 | 【阅读Vue2源码】Template被Vue编译成了什么?)中写的代码,这里也是把代码放进miniCompiler函数中

function miniCompiler(el) {
  function compile(template = '') {
    const ast = parse(template);
    // console.log('alan->ast', ast);
    const code = generate(ast);
    const render = createFunction(code);
    return render;
  }

  function parse(template = '') {
    // 获取元素所有属性
    function getAttrs(el) {
      const attributes = el.attributes;
      const attrs = []; // 收集属性
      const attrMap = {}; // 收集属性的map
      const events = {}; // 收集事件@xxx
      let ifStatment = {}; // 收集v-if
      for (const key in attributes) {
        if (Object.hasOwnProperty.call(attributes, key)) {
          const item = attributes[key];
          attrMap[item.name] = item.value;
          attrs.push({
            name: item.name,
            value: item.value,
          });
          if (item.name.startsWith('@')) { // 处理事件
            events[item.name.replace('@', '')] = { value: item.value }
          }
          if (item.name === 'v-if') { // 处理v-if
            ifStatment = { exp: item.value }
          }
        }
      }

      return { attrs, attrMap, events, ifStatment };
    }

    // 解析插值
    function parseExpressionVar(str = "") {
      const content = ".*?";
      const reg = new RegExp(`{{(${content})}}`, "g");
      const matchs = [...str.matchAll(reg)] || [];
      const res = [];
      if (matchs.length) {
        matchs.forEach((item) => {
          res.push({
            raw: item[0],
            name: String(item[1]).trim(),
            index: item.index,
          });
        });
      }
      return res;
    }

    // 遍历元素
    function walkElement(el, parent) {
      const ast = createASTElement();
      ast.parent = parent;
      ast.tag = el.tagName.toLowerCase();
      // 获取当前元素的所有属性
      const { attrs, attrMap, events, ifStatment } = getAttrs(el);
      ast.attrs = attrs;
      ast.attrMap = attrMap;
      ast.events = events;
      if (ifStatment && Object.keys(ifStatment).length) { // 收集v-if
        ast.if = ifStatment
      }
      const children = Array.from(el.children);
      if (children.length) { // 如果有子元素,递归遍历收集所有子元素
        children.forEach((child) => {
          const childAST = walkElement(child, ast);
          ast.children.push(childAST);
        });
      } else { // 没有子元素,那么就是文本内容,例如:<div>123</div>中的123
        const childVNodes = [...el.childNodes];
        if (childVNodes.length) {
          const text = childVNodes[0].nodeValue
            .trim()
            .replace(" ", "")
            .replace("\n", " ")
            .trim(); // 去除空格和换行
          // 创建空的ast,文本节点增加text属性
          const textAst = createASTElement();
          textAst.text = text;
          textAst.expression = {
            values: parseExpressionVar(el.innerText), // 解析插值{{}}中的值,如果有{{}}
          };
          ast.children.push(textAst);
        }
      }
      return ast;
    }

    const tempDOM = document.createElement("div");
    tempDOM.innerHTML = template;
    const templateDOM = tempDOM.children[0];

    const ast = walkElement(templateDOM, null);
    return ast;
  }

  // 将ast转化成render函数的函数体的字符串
  function generate(ast = {}) {

    // 构建子元素
    const genElmChildren = (children = []) => {
      let str = "[";
      children.forEach((child, i) => {
        str += genElm(child) + `${i == children.length - 1 ? "" : ", "}`;
      });
      return str + "]";
    };

    // 构建data
    const genData = (ast = {}) => {
      const data = {}
      // 处理事件
      if (ast.events && Object.keys(ast.events).length) {
        data.on = ast.events;
      }
      // 处理属性
      if (ast.attrs && ast.attrs.length) {
        data.attrs = {}
        ast.attrs.forEach(item => {
          const skip = item.name.startsWith('@') || item.name === 'v-if'; // 跳过@xxx和v-if
          let key;
          let value;
          if (!skip) {
            if (item.name.startsWith(':')) { // parse :class
              key = item.name.replace(':', '');
              if (data.attrs[key]) {
                const oldVal = data.attrs[key]
                const valList = JSON.parse(item.value.replaceAll(`'`, `"`) || '[]');
                value = `${oldVal} ${valList.join(' ')}`
              }
            } else {
              key = item.name;
              value = item.value;
            }
          }
          data.attrs[key] = value;
        })
      }

      return data;
    };

    // 构建_c()
    const genElm = (ast) => {
      let str = "";
      if (ast['if'] && ast['if'].exp) { // 处理v-if
        let elStr = ''
        if (ast.tag) {
          elStr += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
        }
        // v-if构造出来,就是拼接一个三元运算符,例如count % 2 === 0 ? _c(xxx) : _e()
        str += `${ast['if'].exp} ? ${elStr} : _e()`
      } else if (ast.tag) {
        // 处理元素节点,data参数通过genData函数处理,children通过genElmChildren处理
        str += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
      } else if (ast.text) { // 处理文本节点
        // 处理文本中插值语法,例如:将countVal:{{count}}解析生成'countVal:'+ _s(count)
        if (ast.expression && ast.expression.values.length) {
          // 解析插值语法
          const replaceVarWithFn = (name, target = "") => {
            const toReplace = `' + _s(${name})`;
            const content = ".*?";
            const reg = new RegExp(`{{(${content})}}`, "g");
            let newStr = "";
            newStr = target.replaceAll(reg, (item) => {
              const matchs = [...item.matchAll(reg)] || [];
              let tempStr = "";
              if (matchs.length) {
                matchs.forEach((matItem) => {
                  const mated = matItem[1];
                  if (mated && mated.trim() === name) {
                    tempStr = item.replaceAll(reg, toReplace);
                  }
                });
              }
              return tempStr;
            });
            return newStr;
          };
          let varName = "";
          ast.expression.values.forEach((item) => {
            varName += replaceVarWithFn(item.name, ast.text);
          });
          str += `_v('${varName})`;
        } else {
          // 静态文本
          str += `_v('${ast.text}')`;
        }
      }
      return str;
    };

    let code = genElm(ast);
    return code;
  }

  function createASTElement(tag, attrs, parent) {
    return {
      tag,
      attrsMap: {},
      parent,
      children: []
    }
  }

  function createFunction(code = '') {
    return new Function(`
        with(this) {
          return ${code};
        }
      `)
  }

  // 获取元素和模板字符串
  el = el || document.getElementById('app');
  const template = el.outerHTML;

  // 执行编译
  const compiled = compile(template);

  return {
    render: compiled
  }
}

创建miniRender

因为更新视图的函数需要在mount中实现,所以我这里把mountupdateComponent函数都放进miniRender函数,并返回

function miniRender() {
  function mount(el) {
    if (!(el instanceof HTMLElement)) {
      el = document.querySelector(el);
    }

    updateComponent(this, el);
  }

  function updateComponent(vm, el) {
    function update(vm, el) {
      // 获取render函数
      const { render } = miniCompiler(el);
      class VNode {
        constructor(tag, data, children) {
          this.tag = tag;
          this.data = data;
          this.children = children;
          this.elm = undefined;
          this.context = undefined;
          this.text = undefined;
        }
      }

      // 处理视图更新
      function patch(vm, oldVNode, vnode, parentElm) {
        if (!parentElm) {
          parentElm = document.body;
        }
        if (!oldVNode) {
          oldVNode = vm.$el;
          const emptyVNode = new VNode(oldVNode.tagName.toLowerCase(), {}, []);
          oldVNode = emptyVNode;
          oldVNode.elm = vm.$el;
        }

        // 根据vnode创建真实的dom,处理属性、事件、静态文本、创建子元素等
        const createElm = (vnode, parentElm) => {
          const { tag, data = {}, children = [] } = vnode || {};
          if (tag) {
            const elm = document.createElement(tag);
            const { attrs = {}, on = {} } = data;
            // 处理属性
            for (const key in attrs) {
              if (Object.hasOwnProperty.call(attrs, key)) {
                const value = attrs[key];
                elm.setAttribute(key, value);
              }
            }
            // 处理监听事件
            for (const key in on) {
              if (Object.hasOwnProperty.call(on, key)) {
                if (on[key].value) {
                  const event = vm[on[key].value]
                  event && elm.addEventListener(key, event)
                }
              }
            }
            // 处理子元素
            if (children && children.length) {
              children.forEach((childVNode) => {
                createElm(childVNode, elm);
              });
            }
            vnode.elm = elm;
            // 移除文档上的旧节点
            parentElm.appendChild(elm);
          } else if (vnode.text) {
            // 处理静态文本
            textNode = document.createTextNode(vnode.text);
            parentElm.innerHTML = textNode.nodeValue;
          }
        };

        createElm(vnode, parentElm);
        parentElm.removeChild(oldVNode.elm);
      }

      // 创建vnode
      function createElement(tag = "div", data = {}, children = []) {
        const createVNode = (tag = "div", data = {}, children = []) => {
          const vnodeChildren = [];

          if (children && children.length) {
            children.forEach((child) => {
              vnodeChildren.push(child);
            });
          }
          const vnode = new VNode(tag, data, vnodeChildren);

          return vnode;
        };

        // render函数中执行_c,接收参数,创建vnode
        const vnode = createVNode(tag, data, children);
        return vnode;
      }

      function _c(tag = "div", data = {}, children = []) {
        return createElement(tag, data, children)
      }

      function _v(str) {
        const vnode = new VNode();
        vnode.text = str;
        return vnode;
      }

      function _s(val) {
        return String(val);
      }

      function _e() {
        return new VNode();
      }

      // 挂载render函数中需要使用的_c、_v、_s、_e
      vm._c = _c;
      vm._v = _v;
      vm._s = _s;
      vm._e = _e;

      // 执行渲染函数生成vnode
      const vnode = render.call(vm);

      // 将vnode转成真实的DOM元素
      patch(vm, vm._vnode, vnode, null);
      // 保存旧的vnode
      vm._vnode = vnode;
    }

    update(vm, el);
  }

  return {
    mount,
    updateComponent
  }
}

解析代码逻辑:

挂载元素时执行mount,其实也是执行updateComponent函数,然后Vue初始化完成后更新视图时也是执行updateComponent函数,updateComponent调用update方法

update函数,里面主要包括了

  1. render,调用miniCompiler生成render函数
  2. 定义了VNode
  3. 定义patch函数,处理视图更新的主要逻辑
  4. 定义了createElement,由render函数调用
  5. 定义了render函数需要使用的_c_s_v_e函数
  6. 执行render生成vnode
  7. 执行patch,接收vm,旧vnode,新vnode,父元素作为参数,更新视图
  8. 保存旧的vnodevm._vnode中,用于下次更新时用

patch函数,处理视图更新,主要逻辑为:

  1. 执行createElm,根据vnode创建真实的dom,处理属性、事件、静态文本、创建子元素等,生成真实的DOM,挂载到vnode.elm
  2. 移除文档中旧的元素节点
  3. 视图更新完成

实现效果

总结

  1. 实现的过程还是很复杂的,我利用工作之余的时间加周末,阅读源码加自己动手实现,也花了半个月左右的时间
  2. 要写好文章也很难,要记录自己对源码理解、也要关注写的文章其他人能否读懂,真的挺难。挺耗时间的。
  3. 但是又总会想到雷总的一句话

加油!

附录

完整代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mini Compiler</title>
</head>

<body>
  <section id="app">
    <button @click="plus">+1</button>
    <div class="count-cls" :class="['count-text']">count:{{ count }}</div>
    <div :calss="['count-double']" v-if="count % 2 === 0">doubleCount:{{ count * 2 }}</div>
  </section>

  <script>
    function miniCompiler(el) {
      function compile(template = '') {
        const ast = parse(template);
        // console.log('alan->ast', ast);
        const code = generate(ast);
        const render = createFunction(code);
        return render;
      }

      function parse(template = '') {
        // 获取元素所有属性
        function getAttrs(el) {
          const attributes = el.attributes;
          const attrs = []; // 收集属性
          const attrMap = {}; // 收集属性的map
          const events = {}; // 收集事件@xxx
          let ifStatment = {}; // 收集v-if
          for (const key in attributes) {
            if (Object.hasOwnProperty.call(attributes, key)) {
              const item = attributes[key];
              attrMap[item.name] = item.value;
              attrs.push({
                name: item.name,
                value: item.value,
              });
              if (item.name.startsWith('@')) { // 处理事件
                events[item.name.replace('@', '')] = { value: item.value }
              }
              if (item.name === 'v-if') { // 处理v-if
                ifStatment = { exp: item.value }
              }
            }
          }

          return { attrs, attrMap, events, ifStatment };
        }

        // 解析插值
        function parseExpressionVar(str = "") {
          const content = ".*?";
          const reg = new RegExp(`{{(${content})}}`, "g");
          const matchs = [...str.matchAll(reg)] || [];
          const res = [];
          if (matchs.length) {
            matchs.forEach((item) => {
              res.push({
                raw: item[0],
                name: String(item[1]).trim(),
                index: item.index,
              });
            });
          }
          return res;
        }

        // 遍历元素
        function walkElement(el, parent) {
          const ast = createASTElement();
          ast.parent = parent;
          ast.tag = el.tagName.toLowerCase();
          // 获取当前元素的所有属性
          const { attrs, attrMap, events, ifStatment } = getAttrs(el);
          ast.attrs = attrs;
          ast.attrMap = attrMap;
          ast.events = events;
          if (ifStatment && Object.keys(ifStatment).length) { // 收集v-if
            ast.if = ifStatment
          }
          const children = Array.from(el.children);
          if (children.length) { // 如果有子元素,递归遍历收集所有子元素
            children.forEach((child) => {
              const childAST = walkElement(child, ast);
              ast.children.push(childAST);
            });
          } else { // 没有子元素,那么就是文本内容,例如:<div>123</div>中的123
            const childVNodes = [...el.childNodes];
            if (childVNodes.length) {
              const text = childVNodes[0].nodeValue
                .trim()
                .replace(" ", "")
                .replace("\n", " ")
                .trim(); // 去除空格和换行
              // 创建空的ast,文本节点增加text属性
              const textAst = createASTElement();
              textAst.text = text;
              textAst.expression = {
                values: parseExpressionVar(el.innerText), // 解析插值{{}}中的值,如果有{{}}
              };
              ast.children.push(textAst);
            }
          }
          return ast;
        }

        const tempDOM = document.createElement("div");
        tempDOM.innerHTML = template;
        const templateDOM = tempDOM.children[0];

        const ast = walkElement(templateDOM, null);
        return ast;
      }

      // 将ast转化成render函数的函数体的字符串
      function generate(ast = {}) {

        // 构建子元素
        const genElmChildren = (children = []) => {
          let str = "[";
          children.forEach((child, i) => {
            str += genElm(child) + `${i == children.length - 1 ? "" : ", "}`;
          });
          return str + "]";
        };

        // 构建data
        const genData = (ast = {}) => {
          const data = {}
          // 处理事件
          if (ast.events && Object.keys(ast.events).length) {
            data.on = ast.events;
          }
          // 处理属性
          if (ast.attrs && ast.attrs.length) {
            data.attrs = {}
            ast.attrs.forEach(item => {
              const skip = item.name.startsWith('@') || item.name === 'v-if'; // 跳过@xxx和v-if
              let key;
              let value;
              if (!skip) {
                if (item.name.startsWith(':')) { // parse :class
                  key = item.name.replace(':', '');
                  if (data.attrs[key]) {
                    const oldVal = data.attrs[key]
                    const valList = JSON.parse(item.value.replaceAll(`'`, `"`) || '[]');
                    value = `${oldVal} ${valList.join(' ')}`
                  }
                } else {
                  key = item.name;
                  value = item.value;
                }
              }
              data.attrs[key] = value;
            })
          }

          return data;
        };

        // 构建_c()
        const genElm = (ast) => {
          let str = "";
          if (ast['if'] && ast['if'].exp) { // 处理v-if
            let elStr = ''
            if (ast.tag) {
              elStr += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
            }
            // v-if构造出来,就是拼接一个三元运算符,例如count % 2 === 0 ? _c(xxx) : _e()
            str += `${ast['if'].exp} ? ${elStr} : _e()`
          } else if (ast.tag) {
            // 处理元素节点,data参数通过genData函数处理,children通过genElmChildren处理
            str += `_c('${ast.tag}', ${JSON.stringify(genData(ast))}, ${ast.children ? genElmChildren(ast.children) || "[]" : "[]"})`;
          } else if (ast.text) { // 处理文本节点
            // 处理文本中插值语法,例如:将countVal:{{count}}解析生成'countVal:'+ _s(count)
            if (ast.expression && ast.expression.values.length) {
              // 解析插值语法
              const replaceVarWithFn = (name, target = "") => {
                const toReplace = `' + _s(${name})`;
                const content = ".*?";
                const reg = new RegExp(`{{(${content})}}`, "g");
                let newStr = "";
                newStr = target.replaceAll(reg, (item) => {
                  const matchs = [...item.matchAll(reg)] || [];
                  let tempStr = "";
                  if (matchs.length) {
                    matchs.forEach((matItem) => {
                      const mated = matItem[1];
                      if (mated && mated.trim() === name) {
                        tempStr = item.replaceAll(reg, toReplace);
                      }
                    });
                  }
                  return tempStr;
                });
                return newStr;
              };
              let varName = "";
              ast.expression.values.forEach((item) => {
                varName += replaceVarWithFn(item.name, ast.text);
              });
              str += `_v('${varName})`;
            } else {
              // 静态文本
              str += `_v('${ast.text}')`;
            }
          }
          return str;
        };

        let code = genElm(ast);
        return code;
      }

      function createASTElement(tag, attrs, parent) {
        return {
          tag,
          attrsMap: {},
          parent,
          children: []
        }
      }

      function createFunction(code = '') {
        return new Function(`
            with(this) {
              return ${code};
            }
          `)
      }

      // 获取元素和模板字符串
      el = el || document.getElementById('app');
      const template = el.outerHTML;

      // 执行编译
      const compiled = compile(template);
      // console.log('alan->compiled', compiled);
      return {
        render: compiled
      }
    }

    function createMiniVue(config) {
      const {mount, updateComponent} = config;
      MiniVue.prototype.$mount = mount;

      class MiniWatcher {
        vm = null; // 当前vue/vue组件实例
        cb = () => { }; // 回调函数
        getter = () => { }; // 取值函数
        expression = ''; // watch的键名
        user = false; // 是否是用户定义的watch
        value; // 当前观察的属性的值

        constructor(vm, expOrFn, cb, options = {}) {
          this.vm = vm;
          this.cb = cb;
          this.expression = expOrFn;
          // 对getter做区分处理
          if (typeof expOrFn === 'function') {
            this.getter = expOrFn;
          } else {
            this.getter = parseExpression(this.expression, vm, this);
          }
          this.user = options.user;
          // 初始化lazy
          this.lazy = !!options.lazy;
          // 增加对computed的处理
          this.value = this.lazy ? undefined : this.get();
        }

        get() {
          const value = this.getter.call(this.vm);
          return value;
        }

        update() {
          nextTick(() => {
            this.run();
          })
        }

        run() {
          // 获取新值和旧值
          const newValue = this.get();
          const oldValue = this.value;
          this.value = newValue;
          this.cb.call(this.vm, newValue, oldValue);
          if (this.vm._renderWatcher) {
            this.vm._renderWatcher.cb();
          }
        }

        // 新增computed用的计算值的函数
        evaluate() {
          this.value = this.get();
        }
      }

      class MiniDep {
        static target = null;
        subs = [];

        depend(sub) {
          if (sub && !this.subs.includes(sub)) {
            this.subs.push(sub);
          }
        }

        notify() {
          this.subs.forEach(sub => {
            sub && sub.update();
          })
          MiniDep.target = null;
        }
      }

      // 解析表达式,返回一个函数
      function parseExpression(key, vm, watcher) {
        return () => {
          MiniDep.target = watcher;
          // 取值,触发getter,取值前先把watcher实例放到target中
          const value = vm.data[key];
          // 取完值后,清空Dep.target
          MiniDep.target = null;
          return value;
        }
      }

      function nextTick(cb) {
        return Promise.resolve().then(cb);
      }

      function MiniVue(options = {}) {
        const vm = this;
        this.$el = options.el;
        this.vm = this;
        this.data = options.data;
        this.watch = options.watch;
        this.deps = new Set();

        initData(vm, this.data); // 初始化data
        initWatch(this.watch); // 初始化watch
        initMethods(options.methods); // 初始化methods

        function observe(data) {
          for (const key in data) {
            defineReactive(data, key);
          }
        }

        function defineReactive(data, key) {
          const dep = new MiniDep();
          vm.deps.add(dep);
          const clonedData = JSON.parse(JSON.stringify(data));
          Object.defineProperty(data, key, {
            get: function reactiveGetter() {
              dep.depend(MiniDep.target || vm._renderWatcher);
              return clonedData[key];
            },
            set: function reactiveSetter(value) {
              dep.notify();
              clonedData[key] = value;
              return value;
            }
          });
        }

        function initData(vm, data = {}) {
          for (const key in data) {
            Object.defineProperty(vm, key, {
              configurable: true,
              enumerable: true,
              get() {
                return vm['data'][key];
              },
              set(val) {
                vm['data'][key] = val;
              }
            })
            observe(vm.data);
          }
        }

        function initWatch(watch = {}) {
          for (const key in watch) {
            new MiniWatcher(vm, key, watch[key], { user: true }); // user = true,标记这是用户定义的watch
          }
        }

        function initMethods(methods = {}) {
          for (const key in methods) {
            if (Object.hasOwnProperty.call(methods, key)) {
              const method = methods[key];
              Object.defineProperty(vm, key, {
                value: method.bind(vm)
              })
            }
          }
        }

        vm._renderWatcher = new MiniWatcher(this, () => { }, () => {
          updateComponent(vm, vm.$el)
        })
      }

      return {
        MiniVue
      };
    }

    function miniRender() {
      function mount(el) {
        if (!(el instanceof HTMLElement)) {
          el = document.querySelector(el);
        }
  
        updateComponent(this, el);
      }
  
      function updateComponent(vm, el) {
        function update(vm, el) {
          const { render } = miniCompiler(el);
          class VNode {
            constructor(tag, data, children) {
              this.tag = tag;
              this.data = data;
              this.children = children;
              this.elm = undefined;
              this.context = undefined;
              this.text = undefined;
            }
          }
  
          function patch(vm, oldVNode, vnode, parentElm) {
            if (!parentElm) {
              parentElm = document.body;
            }
            if (!oldVNode) {
              oldVNode = vm.$el;
              const emptyVNode = new VNode(oldVNode.tagName.toLowerCase(), {}, []);
              oldVNode = emptyVNode;
              oldVNode.elm = vm.$el;
            }
            const createElm = (vnode, parentElm) => {
              const { tag, data = {}, children = [] } = vnode || {};
              if (tag) {
                const elm = document.createElement(tag);
                const { attrs = {}, on = {} } = data;
                for (const key in attrs) {
                  if (Object.hasOwnProperty.call(attrs, key)) {
                    const value = attrs[key];
                    elm.setAttribute(key, value);
                  }
                }
                for (const key in on) {
                  if (Object.hasOwnProperty.call(on, key)) {
                    if (on[key].value) {
                      const event = vm[on[key].value]
                      event && elm.addEventListener(key, event)
                    }
                  }
                }
                if (children && children.length) {
                  children.forEach((childVNode) => {
                    createElm(childVNode, elm);
                  });
                }
                vnode.elm = elm;
                parentElm.appendChild(elm);
              } else if (vnode.text) {
                textNode = document.createTextNode(vnode.text);
                parentElm.innerHTML = textNode.nodeValue;
              }
            };
  
            createElm(vnode, parentElm);
            parentElm.removeChild(oldVNode.elm);
          }
  
          function createElement(tag = "div", data = {}, children = []) {
            const createVNode = (tag = "div", data = {}, children = []) => {
              const vnodeChildren = [];
  
              if (children && children.length) {
                children.forEach((child) => {
                  vnodeChildren.push(child);
                });
              }
              const vnode = new VNode(tag, data, vnodeChildren);
  
              return vnode;
            };
  
            const vnode = createVNode(tag, data, children);
            return vnode;
          }
  
          function _c(tag = "div", data = {}, children = []) {
            return createElement(tag, data, children)
          }
  
          function _v(str) {
            const vnode = new VNode();
            vnode.text = str;
            return vnode;
          }
  
          function _s(val) {
            return String(val);
          }
  
          function _e() {
            return new VNode();
          }
  
          // 挂载render函数中需要使用的_c、_v、_s、_e
          vm._c = _c;
          vm._v = _v;
          vm._s = _s;
          vm._e = _e;

          // 执行渲染函数生成vnode
          const vnode = render.call(vm);
  
          // 将vnode转成真实的DOM元素
          patch(vm, vm._vnode, vnode, null);
          vm._vnode = vnode;
        }
  
        update(vm, el);
      }

      return {
        mount,
        updateComponent
      }
    }

    const renderHelpers = miniRender();
    const { MiniVue } = createMiniVue(renderHelpers);

    const app = new MiniVue({
      el: document.getElementById("app"),
      data: {
        count: 0
      },
      methods: {
        plus() {
          this.count += 1;
        }
      }
    })
    app.$mount('#app');

  </script>
</body>

</html>