vue3 - mini-vue(十二)初始化element主流程 & 代理this

312 阅读1分钟

initElement

调用patch打补丁的时候,如果虚拟DOM的type属性是 string,那么这个虚拟DOM描述的则是一个HTML元素,调用processElement来处理。

processElement

  • 调用mountElement(vNode, container)将虚拟DOM挂载到对应容器中

  • 调用 document.createElement 创建对应的真实DOM - el

  • 如果虚拟DOM的 children 是string类型,直接赋值给 el 的 textContent 属性

  • 如果虚拟DOM的 children 是一个数组,说明由多个子元素,调用mountChildren处理 - mountChildren(vNode, el)

  • 遍历 vNode 的props中,对每一个子元素执行patch操作

  • 最后循环vNode.props将虚拟DOM的属性添加到 el 上,最后将 el 添加到container中

processElement - 对应代码

function processElement (vNode, container) {
  mountElement(vNode, container)
}

function mountElement(vNode, container) {
  // $el:这里的虚拟节点时element类型的,也就是App中的根元素div;return instance.vNode.el中的虚拟节点是组件实例对象的虚拟节点
  // 创建对应的DOM,同时绑定到虚拟DOM上
  const el = vNode.el = document.createElement(vNode.type)
  // 处理子节点 - 如果是string类型直接赋值给DOM元素的textContent属性
  if (typeof vNode.children === 'string') {
    el.textContent = vNode.children
  } else if (Array.isArray(vNode.children)) {
    // 如果是数组类型(说明有多个子元素),调用patch递归处理子节点
    mountChildren(vNode, el)
  }
  // 处理vNode对应的属性
  for (const key in vNode.props) {
    el.setAttribute(key, vNode.props[key])
  }
  // 将DOM添加到对应容器中
  container.appendChild(el)
}

// 当children为数组时,处理子节点
function mountChildren (vNode, container) {
  vNode.children.forEach(child => {
    patch(child, container)
  })
}

拦截this

App组件的render方法 返回一个虚拟DOM,return h('div',{}, 'hello,' + this.msg),这个虚拟DOM中调用this.msg访问到了setup中的msg属性。

setupStatefulComponent

该方法在处理setup的时候,创建了一个代理对象,并将代理对象添加到实例的proxy属性上。

proxy

当访问 proxy 的时候,会先到组件实例中获取到组件实例的 setupState属性,即setup函数的返回值。如果访问的属性 key 存在于setupState中,那么直接返回setupState[key]

function setupStatefulComponent(instance) {
  // 第一次patch的时候instance.type就是传入的App组件
  const Component = instance.type

  // 创建一个代理对象用来拦截render函数中的this
  instance.proxy = new Proxy({}, {
    get(target, key) {
        const { setupState } = instance
        // 如果setupState中存在这个属性就返回它的值
        if (key in setupState) {
            return setupState[key]
        }
    }
  })

  const { setup } = Component

  if (setup) {
    const setupResult = setup()

    handleSetupResult(instance, setupResult)
  }
}

App组件中的render方法中的this.msg,这里的this指向App组件。如果想让他指向组件实例的 proxy 属性,需要改变它的this指向。通过 instance.render.call(instance.proxy)改变this指向。

function setupRenderEffect (instance, vNode, container) {
  // instance.render 来自于 finishComponentSetup 方法,就是组件的render方法
  // 绑定this,让render中的this指向创建的代理对象
  const subTree = instance.render.call(instance.proxy)
  // vNode -> patch
  // vNode -> element -> mountElement
  patch(subTree, container)

  // subTree指的就是class="root"的根节点
  // 子元素处理完成之后
  vNode.el = subTree.el
}

$el

在组件中通过 this.$el 可以访问到组件对应的真实DOM。

同理:

function setupStatefulComponent(instance) {
  // 第一次patch的时候instance.type就是传入的App组件
  const Component = instance.type

  // 创建一个代理对象用来拦截render函数中的this
  instance.proxy = new Proxy({}, {
    get(target, key) {
        const { setupState } = instance
        // 如果setupState中存在这个属性就返回它的值
        if (key in setupState) {
            return setupState[key]
        }
        if (key === '$el') {
            return instance.vNode.el
        }
    }
  })

  const { setup } = Component

  if (setup) {
    const setupResult = setup()

    handleSetupResult(instance, setupResult)
  }
}

这里通过虚拟DOM的el属性返回真实DOM,所以在这之前需要将真实DOM添加到对应的虚拟DOM的 el 属性中。

在创建真实DOM的时候,将真实DOM添加到虚拟DOM的el属性上: const el = vNode.el = document.createElement(vNode.type)

function mountElement(vNode, container) {
  // $el:这里的虚拟节点时element类型的,也就是App中的根元素div;return instance.vNode.el中的虚拟节点是组件实例对象的虚拟节点
  // 创建对应的DOM,同时绑定到虚拟DOM上
  const el = vNode.el = document.createElement(vNode.type)
  // 处理子节点 - 如果是string类型直接赋值给DOM元素的textContent属性
  if (typeof vNode.children === 'string') {
    el.textContent = vNode.children
  } else if (Array.isArray(vNode.children)) {
    // 如果是数组类型(说明有多个子元素),调用patch递归处理子节点
    mountChildren(vNode, el)
  }
  // 处理vNode对应的属性
  for (const key in vNode.props) {
    el.setAttribute(key, vNode.props[key])
  }
  // 将DOM添加到对应容器中
  container.appendChild(el)
}

添加完之后通过 $el 还是访问不到它对应真实DOM,因为这里的虚拟节点时element类型的,也就是App中的根元素div;return instance.vNode.el中的虚拟节点是组件实例对象的虚拟节点。

function setupRenderEffect (instance, vNode, container) {
  // instance.render 来自于 finishComponentSetup 方法,就是组件的render方法
  // 绑定this,让render中的this指向创建的代理对象
  const subTree = instance.render.call(instance.proxy)
  // vNode -> patch
  // vNode -> element -> mountElement
  patch(subTree, container)

  // subTree指的就是class="root"的根节点
  // 子元素处理完成之后
  vNode.el = subTree.el
}

添加完之后就可以通过this.$el访问到虚拟DOM对应的真实DOM了。