头图

简易的Ref数据响应式+Renderer渲染器

本案例实现的响应式效果非常有限,只做简单的demo演示,本案例既不是基于Object.defineProperty,也不是基于Proxy实现的响应式,而是基于存取器,其实都差不多。

准备一个 __Ref__,它可以将简单数据类型包装为一个响应式对象。

class __Ref {
  #privateValue;
  #privateRely;   // "桶" 收集所有对#privateValue产生依赖的dom元素

  constructor(value) {
    this.#privateValue = value
    this.#privateRely = []
  }

  pushPrivateRely(newRely) {
    this.#privateRely.push(newRely)
  }

  get value() {
    return this.#privateValue
  }

  set value(newValue) {
    this.#privateValue = newValue
    effect(this.#privateRely, newValue)
  }
}

接着,准备一个副作用函数,用于将 #privateRely 中标记的dom节点逐一更新。effect 副作用函数的调用时机发生在ref的值被改变时。

function effect(relyList, newValue) {
  relyList.forEach(item => {
    if (item.innerText) {
      return item.node.innerText = newValue
    }
    if (item.prop) {
      return item.node[prop] = newValue
    }
  })
}

接着是一个对外暴露的函数,它用于初始化一个响应式包装类。这里其实是想减少一个new关键字的写法而特意封装的一个工厂函数,这样只需要ref()就行了。

export function ref(value) {
  return new __Ref(value)
}

最后就是渲染器,它接收一个表示虚拟dom的结构对象和表示挂载点的dom元素。渲染器会遍历虚拟dom中的 props,提取其中的事件和属性(这里并不兼容一些特定属性,例如 style, class )。接着对 children 进行分析,如果为 ref 响应式对象,则在该对象下的 #privateRely 依赖"桶"中标记当前dom节点进行响应式链接,否则直接渲染为一个文本节点。

export function renderer(vnode, container) {
  const el = document.createElement(vnode.tag)

  for (const key in vnode.props) {
    // 提取事件
    const onEvent = /^on/.test(key)
    const eventName = key.substr(2).toLowerCase()
    if (onEvent) {
      el.addEventListener(eventName, vnode.props[key])
      continue
    }

    // 提取props
    el[key] = vnode.props[key].value
    vnode.props[key].pushPrivateRely({
      node: el,
      [key]: true
    })
  }

  // 提取children: 处理响应式数据
  if (vnode.children instanceof __Ref) {
    const children = document.createTextNode(vnode.children.value)
    vnode.children.pushPrivateRely({
      node: el,
      innerText: true
    })
    el.appendChild(children)
  }
  // 处理children-vnodes
  else if (vnode.children instanceof Array) {
    vnode.children.forEach(child => renderer(child, el))
  }
  // 处理静态数据
  else {
    const children = document.createTextNode(vnode.children.toString())
    el.appendChild(children)
  }

  // 挂载元素
  container.appendChild(el)
}

最后一步(渲染器)已经完成了,现在解构出 refrenderer 实现一个简单的渲染器并根据响应式数据渲染内容。

import { ref, renderer } from './myvue.js'

let navList = ['Home', 'News', 'Person']
let logoLabel = ref('Vue.js')

renderer({
  tag: 'div',
  children: [
    {
      tag: 'b',
      children: logoLabel
    },
    ...navList.map(
      (el, i) => ({
        tag: 'span',
        children: el
      })
    )
  ]
}, document.querySelector("#app"))

Ловень
1 声望0 粉丝