Hippy source code analysis -- hippy Vue



Then, in the previous analysis, we returned to the index.js file of the runtime module from the render module.
All the code of index.js is no longer put, but in the previous issue. This issue specifically analyzes where to introduce the corresponding code. At the same time, some very simple code will be skipped in the analysis process.

code analysis

Next to the last place, the following code:

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) {
    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 {

Then set the $Event attribute, which is derived from.. / renderer/native/event.
This is a class (a new feature of ES6) with attributes such as type and bubbles. There are also functions such as stopPropagation to prevent bubbling propagation of events.

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

Three properties mustuseprop, isreservedtag and isunknownelement are set for the Vue object.
These three attributes are quoted from.. / element/index.js.
.. / element/index.js Code:

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

const isReservedTag = makeMap(
  + 'button,div,form,img,input,label,li,p,span,textarea,ul',

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),

  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 = {
  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 {

Two functions of the util module are introduced at the top of the code, one is the initial capital of the string, and the other is the function used for output in the console.
The isReservedTag variable is a mapping of keywords.
The elementMap variable is a Map object.
Many properties are defined in the defaultViewMeta object. It will be used later when generating nodes.

skipAddToDom determines whether to add this element to the DOM structure of the mobile terminal. If it is false, it will not be added to the dom.
isUnaryTag indicates whether the tag is a unary tag. For example, an "input" tag is a text box.
tagNamespace tag namespace.
Whether the canbeleftoptag tag tag can not be closed. It should mean that only one label is needed to work, such as < br >, which does not need to be closed corresponding to < / BR >.
Whether mustUseProp label must have attributes. For example, the img tag must have src attribute indicating the resource location.

The getDefaultComponent function returns a node object corresponding to the data according to the passed in node name, metadata and other parameters, including a render function for rendering.

The normalizeElementName function can normalize the node name, in short, making the name string lowercase.
The registerElement function will register the node information in the Map object elementMap. The operation is more detailed, including various checks, such as whether it has been registered and whether the hyphen naming is repeated with the hump naming after conversion. A Map is added to the Map object only after all checks have been passed. And finally return an object containing node element information.
The getElementMap function is obviously a "getter" to obtain the elementMap.
The getViewMeta function will preferentially query the meta attribute in the elementMap through the passed in element name parameter. If no mapping exists for this element, defaultViewMeta is returned.
The isKnownView function returns whether the elementMap contains the element specified by the parameter.
Canbeleftoptag, as the name suggests, returns the canbeleftoptag attribute of the element corresponding to the parameter.
After that, many are simple functions that return object properties. No more analysis.
Finally, the registerBuiltinElements function is troublesome. The code content involves build-in.js at the same level as elements/index.js.

In fact, it's just by traversing the build from build-in.js_ IN_ Elements object. To register the nodes one by one and add them to the mapping of elementMap.
As for build_ IN_ What does elements contain? We can simply take a look at import and export first.

import * as BUILT_IN_ELEMENTS from './built-in';
export {

Obviously build_ IN_ Elements are commonly used tags in web development.

These attributes in export are objects defined in build-in.js.
Simply look at the codes of several labels:

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

const img = {
  symbol: components.Image,
  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: {
    name: NATIVE_COMPONENT_NAME_MAP[components.Text],
    defaultNativeProps: {
      text: '',
    defaultNativeStyle: {
      color: 4278190080, // Black color(#000), necessary for Android

const label = span;

const p = span;

These are all definitions of label nodes, such as span label, which sets the default text content and the style displayed on the terminal (for example, color:4278190080 is specified here).

Go back to runtime/index.js, and then install Vue.options.directives.

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

Find runtime/directives/index.js according to the path:

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

The entry file of directives has only these two lines. model.js and show.js are two files at the same level as the entry file, and platform directives is the collection of the output of these two files.
model.js Code:

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;
  update(el, { value, oldValue }) {
    el.value = value;
    update(el, value, oldValue);

export {

The output model object contains two functions: inserted and update.
The inserted function will first set the corresponding update function according to different platform systems. If the EL component is a text input component, the default values between the binding parameters el and binding, regulators and other attributes will be bound. If the regulator is not in lazy mode, add an event listener named change to the EL component and throw an event named input.
The update function is the update function of the corresponding system determined by the inserted function. You can update the value of the el component.
There are several specific execution methods of the update function. It is obvious that the code only defines android and ios systems, and the comments are very clear, so they are not explained.

show.js Code:

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) {
    toggle(el, value, vNode, el.__vOriginalDisplay);
  unbind(el, binding, vNode, oldVNode, isDestroy) {
    if (!isDestroy) {
      el.style.display = el.__vOriginalDisplay;

export {

The show.jsexport object contains three functions: bind, update and unbind. In general, it controls the display mode of the node.
If the el component does not define a display mode, the bind function will set it to the default block mode.
The update function updates the displayed value.
unbind is the display value when the last bind was restored.

Generally speaking, platform directives are assigned to Vue.options.directives. It controls the display style and text update of nodes.

Going on, $mount is rewritten to accommodate the compiler.


After the last analysis, this analysis continues to analyze the code in the future, and analyzes the element s module and the directives module inside the runtime module. In the runtime entry file, the attribute of Vue.config is assigned. Directive properties of Vue.options are installed. And modified some properties on the Vue.prototypeVue prototype.

Tags: Javascript

Posted on Sat, 04 Dec 2021 01:02:19 -0500 by jpowermacg4