Hippy源码分析(九)---hippy-vue

2021SC@SDUSC

目录

简述

接着上期分析,从render模块回到runtime模块的index.js文件。
index.js的全部代码不再放了,在上一期有。这一期具体分析到哪里再引入对应的代码。同时分析过程中会跳过一些十分简单的代码。


代码分析

接着上次的地方,下面的代码:

import { Event } from '../renderer/native/event';
Vue.$Event = Event;
class Event {
  constructor(eventName) {
    this.type = eventName;
    this.bubbles = true;
    this.cancelable = true;
    this.eventPhase = false;
    this.timeStamp = Date.now();

    // TODO: Should point to VDOM element.
    this.originalTarget = null;
    this.currentTarget = null;
    this.target = null;

    // Private properties
    this._canceled = false;
  }

  get canceled() {
    return this._canceled;
  }

  stopPropagation() {
    this.bubbles = false;
  }

  preventDefault() {
    if (!this.cancelable) {
      return;
    }
    this._canceled = true;
  }

  /**
   * Old fashioned compatible.
   */
  initEvent(eventName, bubbles = true, cancelable = true) {
    this.type = eventName;
    if (bubbles === false) {
      this.bubbles = false;
    }
    if (cancelable === false) {
      this.cancelable = false;
    }
    return this;
  }
}

export {
  Event,
};

紧接着设置了$Event属性,Event是引自于../renderer/native/event
这是一个类(ES6新特性),里面有typebubbles等属性。也有stopPropagation等函数可以阻止事件的冒泡传播。

// Install platform specific utils
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isUnknownElement = isUnknownElement;

给Vue对象设置了三个属性mustUseProp,isReservedTag,isUnknownElement
这三个属性引自于../element/index.js
../element/index.js代码:

import { makeMap, camelize } from 'shared/util';
import { capitalizeFirstLetter, warn } from '../util';
import * as BUILT_IN_ELEMENTS from './built-in';

const isReservedTag = makeMap(
  'template,script,style,element,content,slot,'
  + 'button,div,form,img,input,label,li,p,span,textarea,ul',
  true,
);

const elementMap = new Map();

const defaultViewMeta = {
  skipAddToDom: false, // The tag will not add to native DOM.
  isUnaryTag: false, // Single tag, such as img, input...
  tagNamespace: '', // Tag space, such as svg or math, not in using so far.
  canBeLeftOpenTag: false, // Able to no close.
  mustUseProp: false, // Tag must have attribute, such as src with img.
  model: null,
  component: null,
};

function getDefaultComponent(elementName, meta, normalizedName) {
  return {
    name: elementName,
    functional: true,
    model: meta.model,
    render(h, { data, children }) {
      return h(normalizedName, data, children);
    },
  };
}

// Methods
function normalizeElementName(elementName) {
  return elementName.toLowerCase();
}

function registerElement(elementName, oldMeta) {
  if (!elementName) {
    throw new Error('RegisterElement cannot set empty name');
  }
  const normalizedName = normalizeElementName(elementName);

  const meta = { ...defaultViewMeta, ...oldMeta };

  if (elementMap.has(normalizedName)) {
    throw new Error(`Element for ${elementName} already registered.`);
  }

  meta.component = {
    ...getDefaultComponent(elementName, meta, normalizedName),
    ...meta.component,
  };

  if (meta.component.name && meta.component.name === capitalizeFirstLetter(camelize(elementName))) {
    warn(`Cannot registerElement with kebab-case name ${elementName}, which converted to camelCase is the same with component.name ${meta.component.name}, please make them different`);
  }

  const entry = {
    meta,
  };
  elementMap.set(normalizedName, entry);
  return entry;
}

function getElementMap() {
  return elementMap;
}

function getViewMeta(elementName) {
  const normalizedName = normalizeElementName(elementName);

  let viewMeta = defaultViewMeta;
  const entry = elementMap.get(normalizedName);

  if (entry && entry.meta) {
    viewMeta = entry.meta;
  }

  return viewMeta;
}

function isKnownView(elementName) {
  return elementMap.has(normalizeElementName(elementName));
}

function canBeLeftOpenTag(el) {
  return getViewMeta(el).canBeLeftOpenTag;
}

function isUnaryTag(el) {
  return getViewMeta(el).isUnaryTag;
}

function mustUseProp(el, type, attr) {
  const viewMeta = getViewMeta(el);
  if (!viewMeta.mustUseProp) {
    return false;
  }
  return viewMeta.mustUseProp(type, attr);
}

function getTagNamespace(el) {
  return getViewMeta(el).tagNamespace;
}

function isUnknownElement(el) {
  return !isKnownView(el);
}

// Register components
function registerBuiltinElements() {
  Object.keys(BUILT_IN_ELEMENTS).forEach((tagName) => {
    const meta = BUILT_IN_ELEMENTS[tagName];
    registerElement(tagName, meta);
  });
}

export {
  isReservedTag,
  normalizeElementName,
  registerElement,
  getElementMap,
  getViewMeta,
  isKnownView,
  canBeLeftOpenTag,
  isUnaryTag,
  mustUseProp,
  getTagNamespace,
  isUnknownElement,
  registerBuiltinElements,
};

代码顶端引入了util模块的两个函数,一个是字符串首字母大写,一个是用于在console输出的函数。
isReservedTag变量是关键字的映射。
elementMap变量是一个Map对象。
defaultViewMeta对象中定义了许多属性。会在之后生成节点时用到。

skipAddToDom决定着是否将此元素加入到移动端的dom结构中,如果为false则不添加到dom。
isUnaryTag标注是否是一个一元标签。比如一个"input"标签就是一个文本框。
tagNamespace标签命名空间。
canBeLeftOpenTag标签是否可以不闭合。应该是指的只需要一个标签就起作用,比如<br>,其不需要</br>与之对应闭合。
mustUseProp标注标签是否必须有属性。比如img标签必须有src属性表明资源位置。

getDefaultComponent函数会根据传入的节点名、元数据等参数返回一个对应数据的节点对象,包含一个render函数用以渲染。

normalizeElementName函数可以规格化节点名称,简而言之就是把名称字符串化为小写。
registerElement函数会把节点在elementMap这个Map对象中注册信息。其中操作比较细节,包含了各种校验,比如是否已经注册过,连字符式命名经转化后是否和驼峰式命名重复。只有通过所有校验后才会在Map对象中添加映射。并且最终返回一个包含了节点元素信息的对象。
getElementMap函数显而易见就是一个“getter”,用以获得elementMap
getViewMeta函数会通过传入的元素名参数优先去elementMap中查询meta属性。如果不存在这个元素的映射则返回defaultViewMeta
isKnownView函数,返回elementMap中是否含有参数指定的元素。
canBeLeftOpenTag顾名思义,返回参数对应元素的canBeLeftOpenTag属性。
之后很多都是简简单单的返回对象属性的函数。不再做分析。
最后registerBuiltinElements函数比较麻烦了,代码内容涉及到了与elements/index.js同级的built-in.js

实际上只是通过遍历引自于built-in.jsBUILT_IN_ELEMENTS对象。来逐一注册其中的节点添加到elementMap的映射中去。
至于BUILT_IN_ELEMENTS包含什么,我们可以简单的先看一下import和export。

//index.js
import * as BUILT_IN_ELEMENTS from './built-in';
//built-in.js
export {
  button,
  div,
  form,
  img,
  input,
  label,
  li,
  p,
  span,
  a,
  textarea,
  ul,
  iframe,
};

显而易见BUILT_IN_ELEMENTS是我们平常做web开发中常用的标签。

而这些export中的属性,都是定义在built-in.js中的对象。
简单看几个标签的代码:

//button:
const button = {
  symbol: components.View,
  component: {
    ...div.component,
    name: NATIVE_COMPONENT_NAME_MAP[components.View],
    defaultNativeStyle: {
      // TODO: Fill border style.
    },
  },
};

//img
const img = {
  symbol: components.Image,
  component: {
    ...div.component,
    name: NATIVE_COMPONENT_NAME_MAP[components.Image],
    defaultNativeStyle: {
      backgroundColor: 0,
    },
    attributeMaps: {
      // TODO: check placeholder or defaultSource value in compile-time wll be better.
      placeholder: {
        name: 'defaultSource',
        propsValue(value) {
          const url = convertImageLocalPath(value);
          if (url
              && url.indexOf(HIPPY_DEBUG_ADDRESS) < 0
              && ['https://', 'http://'].some(schema => url.indexOf(schema) === 0)) {
            warn(`img placeholder ${url} recommend to use base64 image or local path image`);
          }
          return url;
        },
      },
      /**
       * For Android, will use src property
       * For iOS, will convert to use source property
       * At line: hippy-vue/renderer/native/index.js line 196.
       */
      src(value) {
        return convertImageLocalPath(value);
      },
    },
  },
};
//span  p  label
const span = {
  symbol: components.View, // IMPORTANT: Can't be Text.
  component: {
    ...div.component,
    name: NATIVE_COMPONENT_NAME_MAP[components.Text],
    defaultNativeProps: {
      text: '',
    },
    defaultNativeStyle: {
      color: 4278190080, // Black color(#000), necessary for Android
    },
  },
};

const label = span;

const p = span;

这些全是对于标签节点的定义,比如span标签,设定了默认的文本内容及终端上显示的样式(比如在这里规定了color:4278190080)。

回到runtime/index.js,之后又安装了Vue.options.directives

import * as platformDirectives from './directives';
// Install platform runtime directives & components
extend(Vue.options.directives, platformDirectives);

按照路径找到runtime/directives/index.js

export * from './model';
export * from './show';

directives的入口文件只有这两行,model.jsshow.js是与入口文件同级的两个文件,platformDirectives正是这两个文件的输出的集合。
model.js代码:

import { Event } from '../../renderer/native/event';
import Native from '../native';

// FIXME: Android Should update defaultValue while typing for update contents by state.
function androidUpdate(el, value, oldValue) {
  if (value !== oldValue) {
    el.setAttribute('defaultValue', value);
  }
}

// FIXME: iOS doesn't need to update any props while typing, but need to update text when set state.
function iOSUpdate(el, value) {
  if (value !== el.attributes.defaultValue) {
    el.attributes.defaultValue = value;
    el.setAttribute('text', value);
  }
}

// Set the default update to android.
let update = androidUpdate;

const model = {
  inserted(el, binding) {
    // Update the specific platform update function.
    if (Native.Platform === 'ios' && update !== iOSUpdate) {
      update = iOSUpdate;
    }

    if (el.meta.component.name === 'TextInput') {
      el._vModifiers = binding.modifiers;
      // Initial value
      el.attributes.defaultValue = binding.value;

      // Binding event when typing
      if (!binding.modifiers.lazy) {
        el.addEventListener('change', ({ value }) => {
          const event = new Event('input');
          event.value = value;
          el.dispatchEvent(event);
        });
      }
    }
  },
  update(el, { value, oldValue }) {
    el.value = value;
    update(el, value, oldValue);
  },
};

export {
  model,
};

输出的model对象含有两个函数:insertedupdate
inserted函数会先根据平台系统的不同设置对应的update函数,如果el组件是一个文本输入组件,则绑定参数elbinding之间的默认值,调节器等属性。如果调节器不是lazy模式,则给el组件添加事件监听,名称为change,抛出名为input的事件。
update函数内部是执行的由inserted函数决定的对应系统的update函数。可以更新el组件的值。
update函数具体有几种执行方式显而易见,在代码中只有android和ios系统的定义,并且注释很明确,不在讲解。

show.js代码:

function toggle(el, value, vNode, originalDisplay) {
  if (value) {
    vNode.data.show = true;
    el.setStyle('display', originalDisplay);
  } else {
    el.setStyle('display', 'none');
  }
}

const show = {
  bind(el, { value }, vNode) {
    // Set the display be block when undefined
    if (el.style.display === undefined) {
      el.style.display = 'block';
    }
    const originalDisplay = el.style.display === 'none' ? '' : el.style.display;
    el.__vOriginalDisplay = originalDisplay;
    toggle(el, value, vNode, originalDisplay);
  },
  update(el, { value, oldValue }, vNode) {
    if (!value === !oldValue) {
      return;
    }
    toggle(el, value, vNode, el.__vOriginalDisplay);
  },
  unbind(el, binding, vNode, oldVNode, isDestroy) {
    if (!isDestroy) {
      el.style.display = el.__vOriginalDisplay;
    }
  },
};

export {
  show,
};

show.jsexport了show对象,其中包含了bindupdateunbind三个函数。总的来说是控制节点的显示(display)模式。
bind函数如果在el组件没有定义显示模式时则会设其为默认的block模式。
update函数更新显示的值。
unbind是恢复上一次bind时的display值。

总的来说大致是把platformDirectives赋给了Vue.options.directives。控制着节点的显示样式与文本更新等内容。

继续往下,为了适应编译器重写了$mount。


总结

接上次分析到的位置,此次分析继续往后分析代码,分析到了element模块和runtime模块内部的directives模块。在runtime入口文件中,又对Vue.config进行了属性赋值。安装了Vue.options的指令属性。并修改了Vue.prototypeVue原型上的一些属性。

上一篇:来来来,手摸手写一个hook


下一篇:react源码解析14.手写hooks