IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Hippy源码分析(八)---hippy-vue -> 正文阅读

[JavaScript知识库]Hippy源码分析(八)---hippy-vue

2021SC@SDUSC

简述

上一期简单的开始了hippy-vue源码的分析,并且分析完了util模块。
回顾hippy-vue的入口文件index.js中,除了使用了util模块,另一个,也是目前最重要的就是runtime模块了。
入口文件index.js回顾↓:
在这里插入图片描述
在这里插入图片描述
所以这一期进行runtime模块源代码的分析。
先看一下目录结构:
在这里插入图片描述

各个文件的具体功能此处暂时不作分析。
单独提一下node-ops.js:

hippy-vue 其实是基于官方 Vue 2.x 源代码,通过改写 node-ops 外挂实现的自定义渲染层,但不仅仅是个到终端的渲染层,还同时实现前端组件到终端的映射、CSS 语法解析,和其它跨端框架不同,它尽力将 Web 端的开发体验带到终端上来,同时保持了对 Web 生态的兼容。

由此可见node-ops是hippy的关键点,实现了自定义渲染层,以支持vue的组件等可以映射到终端。

再放一张hippy-vue官网里的一张架构图:
在这里插入图片描述

这期先分析runtime模块的index.js文件。从index.js逐渐分析其余代码。

代码分析

先上runtime/index.js源代码:

import Vue from 'core/index';
import { defineComputed, proxy } from 'core/instance/state';
import { ASSET_TYPES } from 'shared/constants';
import { mountComponent } from 'core/instance/lifecycle';
import { compileToFunctions } from 'web/compiler/index';
import {
  warn,
  isPlainObject,
  mergeOptions,
  extend,
} from 'core/util/index';
import {
  registerBuiltinElements,
  registerElement,
  getElementMap,
  mustUseProp,
  isReservedTag,
  isUnknownElement,
} from '../elements';
import {
  getApp,
  setApp,
  isFunction,
  trace,
  setBeforeLoadStyle,
} from '../util';
import DocumentNode from '../renderer/document-node';
import { Event } from '../renderer/native/event';
import { patch } from './patch';
import Native, { HippyRegister } from './native';
import * as iPhone from './iphone';
import * as platformDirectives from './directives';

const componentName = ['%c[Hippy-Vue process.env.HIPPY_VUE_VERSION]%c', 'color: #4fc08d; font-weight: bold', 'color: auto; font-weight: auto'];

// Install document
const documentNode = new DocumentNode();
Vue.$document = documentNode;
Vue.prototype.$document = documentNode;

// Install document and event classes
Vue.$Document = DocumentNode;
Vue.$Event = Event;

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

Vue.compile = compileToFunctions;
Vue.registerElement = registerElement;

// Install platform runtime directives & components
extend(Vue.options.directives, platformDirectives);

// install platform patch function
Vue.prototype.__patch__ = patch;

// Override $mount for attend the compiler.
Vue.prototype.$mount = function $mount(el, hydrating) {
  const options = this.$options;
  // resolve template/el and convert to render function
  if (!options.render) {
    const { template } = options;
    if (template && typeof template !== 'string') {
      warn(`invalid template option: ${template}`, this);
      return this;
    }

    if (template) {
      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          delimiters: options.delimiters,
          comments: options.comments,
        },
        this,
      );
      options.render = render;
      options.staticRenderFns = staticRenderFns;
    }
  }

  return mountComponent(this, el, hydrating);
};

/**
 * Register the Hippy-Vue app to Native.
 *
 * @param {function} callback - Callback after register completed.
 */
Vue.prototype.$start = function $start(afterCallback, beforeCallback) {
  setApp(this);

  // beforeLoadStyle is a hidden option for pre-process
  // the style declaration globally.
  if (isFunction(this.$options.beforeLoadStyle)) {
    setBeforeLoadStyle(this.$options.beforeLoadStyle);
  }

  // register native components into Vue.
  getElementMap().forEach((entry) => {
    Vue.component(entry.meta.component.name, entry.meta.component);
  });

  // Register the entry point into Hippy
  // The callback will be exectue when Native trigger loadInstance
  // or runApplication event.
  HippyRegister.regist(this.$options.appName, (superProps) => {
    const { __instanceId__: rootViewId } = superProps;
    this.$options.$superProps = superProps;
    this.$options.rootViewId = rootViewId;

    trace(...componentName, 'Start', this.$options.appName, 'with rootViewId', rootViewId, superProps);

    // Destroy the old instance and set the new one when restart the app
    if (this.$el) {
      this.$destroy();
      const AppConstructor = Vue.extend(this.$options);
      const newApp = new AppConstructor(this.$options);
      setApp(newApp);
    }

    // Call the callback before $mount
    if (isFunction(beforeCallback)) {
      beforeCallback(this, superProps);
    }

    // Draw the app.
    this.$mount();

    // Draw the iPhone status bar background.
    // It should execute after $mount, otherwise this.$el will be undefined.
    if (Native.Platform === 'ios') {
      const statusBar = iPhone.drawStatusBar(this.$options);
      if (statusBar) {
        if (!this.$el.childNodes.length) {
          this.$el.appendChild(statusBar);
        } else {
          this.$el.insertBefore(statusBar, this.$el.childNodes[0]);
        }
      }
    }

    // Call the callback after $mount
    if (isFunction(afterCallback)) {
      afterCallback(this, superProps);
    }
  });
};

// Override component and extend of avoid built-in component warning.
let cid = 1;

function initProps(Comp) {
  const { props } = Comp.options;
  Object.keys(props).forEach(key => proxy(Comp.prototype, '_props', key));
}

function initComputed(Comp) {
  const { computed } = Comp.options;
  Object.keys(computed).forEach(key => defineComputed(Comp.prototype, key, computed[key]));
}

// Override component for avoid built-in component warning.
Vue.component = function component(id, definition) {
  if (!definition) {
    return this.options.components[id];
  }
  if (isPlainObject(definition)) {
    definition.name = definition.name || id;
    definition = this.options._base.extend(definition);
  }
  this.options.components[id] = definition;
  return definition;
};

// Override extend for avoid built-in component warning.
Vue.extend = function hippyExtend(extendOptions) {
  extendOptions = extendOptions || {};
  const Super = this;
  const SuperId = Super.cid;
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId];
  }

  const name = extendOptions.name || Super.options.name;

  const Sub = function VueComponent(options) {
    this._init(options);
  };
  Sub.prototype = Object.create(Super.prototype);
  Sub.prototype.constructor = Sub;
  cid += 1;
  Sub.cid = cid;
  Sub.options = mergeOptions(Super.options, extendOptions);
  Sub.super = Super;

  // For props and computed properties, we define the proxy getters on
  // the Vue instances at extension time, on the extended prototype. This
  // avoids Object.defineProperty calls for each instance created.
  if (Sub.options.props) {
    initProps(Sub);
  }
  if (Sub.options.computed) {
    initComputed(Sub);
  }

  // allow further extension/mixin/plugin usage
  Sub.extend = Super.extend;
  Sub.mixin = Super.mixin;
  Sub.use = Super.use;

  // create asset registers, so extended classes
  // can have their private assets too.
  ASSET_TYPES.forEach((type) => {
    Sub[type] = Super[type];
  });
  // enable recursive self-lookup
  if (name) {
    Sub.options.components[name] = Sub;
  }

  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options;
  Sub.extendOptions = extendOptions;
  Sub.sealedOptions = extend({}, Sub.options);

  // cache constructor
  cachedCtors[SuperId] = Sub;
  return Sub;
};

// Binding Native Properties
Vue.Native = Native;

Vue.getApp = getApp;

// Register the built-in elements
Vue.use(registerBuiltinElements);

export default Vue;

文件最后向外输出了Vue对象。
代码主要干了以下事情:

  1. 安装初始化文档和事件类
  2. 安装初始化平台工具和运行时指令组件
  3. 重写mount、扩展以避免组件内置警告
  4. 绑定Native属性

// Install document部分是new了一个documentNode对象并且赋给Vue的$document属性以及原型上对应的$document。
documentNode函数引自于render模块的document-node.js
render模块目录结构:(这一模块控制着渲染,定义、生成DOM节点等。)
在这里插入图片描述

render/document-node.js代码:

import CommentNode from './comment-node';
import ElementNode from './element-node';
import ViewNode from './view-node';
import TextNode from './text-node';
import InputNode from './input-node';
import ListNode from './list-node';
import ListItemNode from './list-item-node';
import { Event } from './native/event';

class DocumentNode extends ViewNode {
  constructor() {
    super();

    this.documentElement = new ElementNode('document');

    // make static methods accessible via this
    this.createComment = this.constructor.createComment;
    this.createElement = this.constructor.createElement;
    this.createElementNS = this.constructor.createElementNS;
    this.createTextNode = this.constructor.createTextNode;
  }

  static createComment(text) {
    return new CommentNode(text);
  }

  static createElement(tagName) {
    // TODO: create element instance by tagName definition.
    switch (tagName) {
      case 'input':
      case 'textarea':
        return new InputNode(tagName);
      case 'ul':
        return new ListNode(tagName);
      case 'li':
        return new ListItemNode(tagName);
      default:
        return new ElementNode(tagName);
    }
  }

  static createElementNS(namespace, tagName) {
    return new ElementNode(`${namespace}:${tagName}`);
  }

  static createTextNode(text) {
    return new TextNode(text);
  }

  static createEvent(eventName) {
    return new Event(eventName);
  }
}

export default DocumentNode;

这一部分比较简单直接,文件直接向外输出了DocumentNode类。
而这个类是继承于view-node.js文件的ViewNode类。
view-node.js代码:

import { insertChild, removeChild } from './native';

const ROOT_VIEW_ID = 0;
let currentNodeId = 0;
if (global.__GLOBAL__ && Number.isInteger(global.__GLOBAL__.nodeId)) {
  currentNodeId = global.__GLOBAL__.nodeId;
}
function getNodeId() {
  currentNodeId += 1;
  if (currentNodeId % 10 === 0) {
    currentNodeId += 1;
  }
  if (currentNodeId % 10 === ROOT_VIEW_ID) {
    currentNodeId += 1;
  }
  return currentNodeId;
}

class ViewNode {
  constructor() {
    // Point to root document element.
    this._ownerDocument = null;

    // Component meta information, such as native component will use.
    this._meta = null;

    // Will change to be true after insert into Native dom.
    this._isMounted = false;

    // Virtual DOM node id, will used in native to identify.
    this.nodeId = getNodeId();

    // Index number in children, will update at traverseChildren method.
    this.index = 0;

    // Relation nodes.
    this.childNodes = [];
    this.parentNode = null;
    this.prevSibling = null;
    this.nextSibling = null;
  }

  /* istanbul ignore next */
  toString() {
    return this.constructor.name;
  }

  get firstChild() {
    return this.childNodes.length ? this.childNodes[0] : null;
  }

  get lastChild() {
    return this.childNodes.length
      ? this.childNodes[this.childNodes.length - 1]
      : null;
  }

  get meta() {
    if (!this._meta) {
      return {};
    }
    return this._meta;
  }

  /* istanbul ignore next */
  get ownerDocument() {
    if (this._ownerDocument) {
      return this._ownerDocument;
    }

    let el = this;
    while (el.constructor.name !== 'DocumentNode') {
      el = el.parentNode;
      if (!el) {
        break;
      }
    }
    this._ownerDocument = el;
    return el;
  }

  get isMounted() {
    return this._isMounted;
  }

  set isMounted(isMounted) {
    // TODO: Maybe need validation, maybe not.
    this._isMounted = isMounted;
  }

  insertBefore(childNode, referenceNode) {
    if (!childNode) {
      throw new Error('Can\'t insert child.');
    }

    if (!referenceNode) {
      return this.appendChild(childNode);
    }

    if (referenceNode.parentNode !== this) {
      throw new Error('Can\'t insert child, because the reference node has a different parent.');
    }

    if (childNode.parentNode && childNode.parentNode !== this) {
      throw new Error('Can\'t insert child, because it already has a different parent.');
    }

    const index = this.childNodes.indexOf(referenceNode);

    childNode.parentNode = this;
    childNode.nextSibling = referenceNode;
    childNode.prevSibling = this.childNodes[index - 1];

    // update previous node's nextSibling to prevent patch bug
    if (this.childNodes[index - 1]) {
      this.childNodes[index - 1].nextSibling = childNode;
    }

    referenceNode.prevSibling = childNode;
    this.childNodes.splice(index, 0, childNode);

    return insertChild(this, childNode, index);
  }

  moveChild(childNode, referenceNode) {
    if (!childNode) {
      throw new Error('Can\'t move child.');
    }

    if (!referenceNode) {
      return this.appendChild(childNode);
    }

    if (referenceNode.parentNode !== this) {
      throw new Error('Can\'t move child, because the reference node has a different parent.');
    }

    if (childNode.parentNode && childNode.parentNode !== this) {
      throw new Error('Can\'t move child, because it already has a different parent.');
    }

    const oldIndex = this.childNodes.indexOf(childNode);
    const newIndex = this.childNodes.indexOf(referenceNode);

    // return if the moved index is the same as the previous one
    if (newIndex === oldIndex) {
      return childNode;
    }

    // set new siblings relations
    childNode.nextSibling = referenceNode;
    childNode.prevSibling = referenceNode.prevSibling;
    referenceNode.prevSibling = childNode;

    if (this.childNodes[newIndex - 1]) {
      this.childNodes[newIndex - 1].nextSibling = childNode;
    }
    if (this.childNodes[newIndex + 1]) {
      this.childNodes[newIndex + 1].prevSibling = childNode;
    }
    if (this.childNodes[oldIndex - 1]) {
      this.childNodes[oldIndex - 1].nextSibling = this.childNodes[oldIndex + 1];
    }
    if (this.childNodes[oldIndex + 1]) {
      this.childNodes[oldIndex + 1].prevSibling = this.childNodes[oldIndex - 1];
    }

    // remove old child node from native
    removeChild(this, childNode);

    // remove old child and insert new child, which is like moving child
    this.childNodes.splice(newIndex, 0, childNode);
    this.childNodes.splice(oldIndex + (newIndex < oldIndex ? 1 : 0), 1);

    // should filter empty nodes before finding the index of node
    const atIndex = this.childNodes.filter(ch => ch.index > -1).indexOf(childNode);
    return insertChild(this, childNode, atIndex);
  }

  appendChild(childNode) {
    if (!childNode) {
      throw new Error('Can\'t append child.');
    }

    if (childNode.parentNode && childNode.parentNode !== this) {
      throw new Error('Can\'t append child, because it already has a different parent.');
    }

    // remove childNode if exist
    if (childNode.isMounted) {
      this.removeChild(childNode);
    }

    childNode.parentNode = this;

    if (this.lastChild) {
      childNode.prevSibling = this.lastChild;
      this.lastChild.nextSibling = childNode;
    }

    this.childNodes.push(childNode);

    insertChild(this, childNode, this.childNodes.length - 1);
  }

  removeChild(childNode) {
    if (!childNode) {
      throw new Error('Can\'t remove child.');
    }

    if (!childNode.parentNode) {
      throw new Error('Can\'t remove child, because it has no parent.');
    }

    if (childNode.parentNode !== this) {
      throw new Error('Can\'t remove child, because it has a different parent.');
    }

    if (childNode.meta.skipAddToDom) {
      return;
    }

    removeChild(this, childNode);

    // FIXME: parentNode should be null when removeChild, But it breaks add the node again.
    //        Issue position: https://github.com/vuejs/vue/tree/master/src/core/vdom/patch.js#L250
    // childNode.parentNode = null;

    if (childNode.prevSibling) {
      childNode.prevSibling.nextSibling = childNode.nextSibling;
    }

    if (childNode.nextSibling) {
      childNode.nextSibling.prevSibling = childNode.prevSibling;
    }

    childNode.prevSibling = null;
    childNode.nextSibling = null;
    this.childNodes = this.childNodes.filter(node => node !== childNode);
  }

  /**
   * Find a specific target with condition
   */
  findChild(condition) {
    const yes = condition(this);
    if (yes) {
      return this;
    }
    if (this.childNodes.length) {
      for (let i = 0; i < this.childNodes.length; i += 1) {
        const childNode = this.childNodes[i];
        const targetChild = this.findChild.call(childNode, condition);
        if (targetChild) {
          return targetChild;
        }
      }
    }
    return null;
  }

  /**
   * Traverse the children and execute callback
   */
  traverseChildren(callback) {
    // Find the index and apply callback
    let index;
    if (this.parentNode) {
      index = this.parentNode.childNodes.filter(node => !node.meta.skipAddToDom).indexOf(this);
    } else {
      index = 0;
    }
    this.index = index;
    callback(this);

    // Find the children
    if (this.childNodes.length) {
      this.childNodes.forEach((childNode) => {
        this.traverseChildren.call(childNode, callback);
      });
    }
  }
}

export default ViewNode;

ViewNode类是视图节点。
其中除了构造器,主要函数还有各种getset函数用以获取子节点以及自己的渲染状态,insertBeforemoveChildappendChildremoveChild等函数可以修改节点布局,其中removeChild等函数调用了./native/index.js的相应函数。


再看document-node.js代码。
含有一个构造器及createCommentcreateElementcreateElementNS等函数。
其中createElement函数可以根据标签名(例如"li"),new一个相应的节点对象。
这一部分的对象都是引自同一目录下的其他文件代码。有的类继承了ViewNode,如TextNode

//TestNode
import ViewNode from './view-node';
import { Text } from './native/components';

export default class TextNode extends ViewNode {
  constructor(text) {
    super();

    this.text = text;
    this._meta = {
      symbol: Text,
      skipAddToDom: true,
    };
  }

  setText(text) {
    this.text = text;
    this.parentNode.setText(text);
  }
}

有的节点类除了展示还需要其他一些功能,继承的是ElementNode
具体代码结构如上面的TestNode类似。
一个构造器,如果有需要则含有其他一些可以设置这个节点的函数。比如TestNodesetText函数。


总结

到此为止,这一期从runtime模块入手,按照代码路线基本把涉及到的render模块分析完了(native文件夹里的代码还暂时没有分析到,主要就是和移动终端设备的兼容问题,里面有css选择器、解析器、终端组件映射等。),render模块涉及到了dom节点的定义与生成等,为了和移动终端兼容,实现了部分终端常用的节点。
下一期可以继续深入分析runtime的入口文件index.js

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-11-26 08:46:34  更:2021-11-26 08:48:49 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/21 2:02:19-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码